From 37a47ad9400e2e940fcba1f317c3629030b26638 Mon Sep 17 00:00:00 2001 From: Stephen Carrera Date: Wed, 6 Dec 2017 11:00:20 -0800 Subject: [PATCH 1/2] login users --- Unit-02/04-flask-login/app.py | 4 + Unit-02/04-flask-login/manage.py | 12 ++ Unit-02/04-flask-login/migrations/README | 1 + Unit-02/04-flask-login/migrations/alembic.ini | 45 ++++++++ Unit-02/04-flask-login/migrations/env.py | 87 ++++++++++++++ .../04-flask-login/migrations/script.py.mako | 24 ++++ .../32d323b1b2da_create_tags_table.py | 39 +++++++ ...788d47_create_users_and_messages_tables.py | 45 ++++++++ .../migrations/versions/89838541ca52_.py | 32 +++++ Unit-02/04-flask-login/project/__init__.py | 52 +++++++++ .../project/decorators/__init__.py | 31 +++++ .../04-flask-login/project/messages/forms.py | 17 +++ .../project/messages/templates/edit.html | 30 +++++ .../project/messages/templates/index.html | 27 +++++ .../project/messages/templates/new.html | 26 +++++ .../project/messages/templates/show.html | 15 +++ .../04-flask-login/project/messages/views.py | 81 +++++++++++++ Unit-02/04-flask-login/project/models.py | 64 ++++++++++ Unit-02/04-flask-login/project/tags/forms.py | 18 +++ .../project/tags/templates/tags/edit.html | 30 +++++ .../project/tags/templates/tags/index.html | 12 ++ .../project/tags/templates/tags/new.html | 17 +++ .../project/tags/templates/tags/show.html | 19 +++ Unit-02/04-flask-login/project/tags/views.py | 79 +++++++++++++ .../project/templates/base.html | 50 ++++++++ .../project/templates/errors.html | 8 ++ Unit-02/04-flask-login/project/users/forms.py | 16 +++ .../project/users/templates/users/edit.html | 22 ++++ .../project/users/templates/users/index.html | 22 ++++ .../project/users/templates/users/login.html | 24 ++++ .../project/users/templates/users/show.html | 0 .../project/users/templates/users/signup.html | 23 ++++ Unit-02/04-flask-login/project/users/views.py | 109 ++++++++++++++++++ Unit-02/04-flask-login/requirements.txt | 41 +++++++ 34 files changed, 1122 insertions(+) create mode 100644 Unit-02/04-flask-login/app.py create mode 100644 Unit-02/04-flask-login/manage.py create mode 100755 Unit-02/04-flask-login/migrations/README create mode 100644 Unit-02/04-flask-login/migrations/alembic.ini create mode 100755 Unit-02/04-flask-login/migrations/env.py create mode 100755 Unit-02/04-flask-login/migrations/script.py.mako create mode 100644 Unit-02/04-flask-login/migrations/versions/32d323b1b2da_create_tags_table.py create mode 100644 Unit-02/04-flask-login/migrations/versions/5656eb788d47_create_users_and_messages_tables.py create mode 100644 Unit-02/04-flask-login/migrations/versions/89838541ca52_.py create mode 100644 Unit-02/04-flask-login/project/__init__.py create mode 100644 Unit-02/04-flask-login/project/decorators/__init__.py create mode 100644 Unit-02/04-flask-login/project/messages/forms.py create mode 100644 Unit-02/04-flask-login/project/messages/templates/edit.html create mode 100644 Unit-02/04-flask-login/project/messages/templates/index.html create mode 100644 Unit-02/04-flask-login/project/messages/templates/new.html create mode 100644 Unit-02/04-flask-login/project/messages/templates/show.html create mode 100644 Unit-02/04-flask-login/project/messages/views.py create mode 100644 Unit-02/04-flask-login/project/models.py create mode 100644 Unit-02/04-flask-login/project/tags/forms.py create mode 100644 Unit-02/04-flask-login/project/tags/templates/tags/edit.html create mode 100644 Unit-02/04-flask-login/project/tags/templates/tags/index.html create mode 100644 Unit-02/04-flask-login/project/tags/templates/tags/new.html create mode 100644 Unit-02/04-flask-login/project/tags/templates/tags/show.html create mode 100644 Unit-02/04-flask-login/project/tags/views.py create mode 100644 Unit-02/04-flask-login/project/templates/base.html create mode 100644 Unit-02/04-flask-login/project/templates/errors.html create mode 100644 Unit-02/04-flask-login/project/users/forms.py create mode 100644 Unit-02/04-flask-login/project/users/templates/users/edit.html create mode 100644 Unit-02/04-flask-login/project/users/templates/users/index.html create mode 100644 Unit-02/04-flask-login/project/users/templates/users/login.html create mode 100644 Unit-02/04-flask-login/project/users/templates/users/show.html create mode 100644 Unit-02/04-flask-login/project/users/templates/users/signup.html create mode 100644 Unit-02/04-flask-login/project/users/views.py create mode 100644 Unit-02/04-flask-login/requirements.txt diff --git a/Unit-02/04-flask-login/app.py b/Unit-02/04-flask-login/app.py new file mode 100644 index 0000000..d7f70e7 --- /dev/null +++ b/Unit-02/04-flask-login/app.py @@ -0,0 +1,4 @@ +from project import app + +if __name__ == '__main__': + app.run(debug=True,port=3000) \ No newline at end of file diff --git a/Unit-02/04-flask-login/manage.py b/Unit-02/04-flask-login/manage.py new file mode 100644 index 0000000..6468e99 --- /dev/null +++ b/Unit-02/04-flask-login/manage.py @@ -0,0 +1,12 @@ +from project import app, db + +from flask_script import Manager +from flask_migrate import Migrate, MigrateCommand + +migrate = Migrate(app, db) +manager = Manager(app) + +manager.add_command('db', MigrateCommand) + +if __name__ == '__main__': + manager.run() \ No newline at end of file diff --git a/Unit-02/04-flask-login/migrations/README b/Unit-02/04-flask-login/migrations/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/Unit-02/04-flask-login/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/Unit-02/04-flask-login/migrations/alembic.ini b/Unit-02/04-flask-login/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/Unit-02/04-flask-login/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/Unit-02/04-flask-login/migrations/env.py b/Unit-02/04-flask-login/migrations/env.py new file mode 100755 index 0000000..23663ff --- /dev/null +++ b/Unit-02/04-flask-login/migrations/env.py @@ -0,0 +1,87 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/Unit-02/04-flask-login/migrations/script.py.mako b/Unit-02/04-flask-login/migrations/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/Unit-02/04-flask-login/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/Unit-02/04-flask-login/migrations/versions/32d323b1b2da_create_tags_table.py b/Unit-02/04-flask-login/migrations/versions/32d323b1b2da_create_tags_table.py new file mode 100644 index 0000000..13c247b --- /dev/null +++ b/Unit-02/04-flask-login/migrations/versions/32d323b1b2da_create_tags_table.py @@ -0,0 +1,39 @@ +"""create tags table + +Revision ID: 32d323b1b2da +Revises: 5656eb788d47 +Create Date: 2017-12-03 15:44:28.391960 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '32d323b1b2da' +down_revision = '5656eb788d47' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('text', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('message_tags', + sa.Column('message_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ), + sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('message_tags') + op.drop_table('tags') + # ### end Alembic commands ### diff --git a/Unit-02/04-flask-login/migrations/versions/5656eb788d47_create_users_and_messages_tables.py b/Unit-02/04-flask-login/migrations/versions/5656eb788d47_create_users_and_messages_tables.py new file mode 100644 index 0000000..9dc007f --- /dev/null +++ b/Unit-02/04-flask-login/migrations/versions/5656eb788d47_create_users_and_messages_tables.py @@ -0,0 +1,45 @@ +"""create users and messages tables + +Revision ID: 5656eb788d47 +Revises: +Create Date: 2017-12-02 17:24:15.879546 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5656eb788d47' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('first_name', sa.Text(), nullable=True), + sa.Column('last_name', sa.Text(), nullable=True), + sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('messages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), nullable=True), + sa.Column('created_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_on', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('messages') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/Unit-02/04-flask-login/migrations/versions/89838541ca52_.py b/Unit-02/04-flask-login/migrations/versions/89838541ca52_.py new file mode 100644 index 0000000..4677f8e --- /dev/null +++ b/Unit-02/04-flask-login/migrations/versions/89838541ca52_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 89838541ca52 +Revises: 32d323b1b2da +Create Date: 2017-12-04 18:28:37.879257 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '89838541ca52' +down_revision = '32d323b1b2da' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('password', sa.Text(), nullable=True)) + op.add_column('users', sa.Column('username', sa.Text(), nullable=True)) + op.create_unique_constraint(None, 'users', ['username']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'users', type_='unique') + op.drop_column('users', 'username') + op.drop_column('users', 'password') + # ### end Alembic commands ### diff --git a/Unit-02/04-flask-login/project/__init__.py b/Unit-02/04-flask-login/project/__init__.py new file mode 100644 index 0000000..cffa408 --- /dev/null +++ b/Unit-02/04-flask-login/project/__init__.py @@ -0,0 +1,52 @@ +from flask import Flask, redirect, url_for, render_template +from flask_modus import Modus +from flask_sqlalchemy import SQLAlchemy +from flask_moment import Moment +from flask_bcrypt import Bcrypt +from flask_login import LoginManager +import os + + + +app = Flask(__name__) +app.url_map.strict_slashes = False +bcrypt = Bcrypt(app) +login_manager = LoginManager() +login_manager.init_app(app) + +app.config["SQLALCHEMY_DATABASE_URI"] = 'postgres://localhost/users-messages-bcrypt' +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +app.config["SECRET_KEY"] = os.environ.get('SECRET_KEY') + +modus = Modus(app) +moment = Moment(app) +db = SQLAlchemy(app) + + +from project.users.views import users_blueprint +from project.messages.views import messages_blueprint +from project.tags.views import tags_blueprint + +app.register_blueprint(users_blueprint, url_prefix='/users') +app.register_blueprint(messages_blueprint, url_prefix='/users//messages') +app.register_blueprint(tags_blueprint, url_prefix='/tags') + +login_manager.login_view = "users.login" + +from project.models import User + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) + +@app.route('/') +def root(): + return redirect(url_for('users.login')) + +@app.errorhandler(404) +def page_not_found(error): + return render_template('errors.html', error=error), 404 + +@app.errorhandler(500) +def server_error(error): + return render_template('errors.html', error=error), 500 \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/decorators/__init__.py b/Unit-02/04-flask-login/project/decorators/__init__.py new file mode 100644 index 0000000..a528085 --- /dev/null +++ b/Unit-02/04-flask-login/project/decorators/__init__.py @@ -0,0 +1,31 @@ +from functools import wraps +from flask import redirect, url_for, session, flash +from flask_login import current_user + +# def ensure_authenticated(fn): +# @wraps(fn) +# def wrapper(*args, **kwargs): +# if session.get('user_id') is None: +# flash('Please log in first!') +# return redirect(url_for('users.login')) +# return fn(*args,**kwargs) +# return wrapper + +def prevent_login_signup(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if current_user.is_authenticated: + flash('You are logged in already') + return redirect(url_for('users.index')) + return fn(*args, **kwargs) + return wrapper + +def ensure_correct_user(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + correct_id = kwargs.get('id') or kwargs.get('user_id') + if correct_id != current_user.id: + flash('Not Authorized') + return redirect(url_for('users.index', id=current_user.id)) + return fn(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/forms.py b/Unit-02/04-flask-login/project/messages/forms.py new file mode 100644 index 0000000..d86e5b2 --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/forms.py @@ -0,0 +1,17 @@ +from flask_wtf import FlaskForm +from wtforms import TextAreaField, SelectMultipleField, widgets, validators +from project.models import Tag + +class MultiCheckboxField(SelectMultipleField): + widget = widgets.ListWidget(prefix_label=False) + option_widget = widgets.CheckboxInput() + +class MessageForm(FlaskForm): + content=TextAreaField('Content', [validators.DataRequired(), validators.length(max=280)], render_kw={"rows": 3, "class": "text-center col-6"}) + tags = MultiCheckboxField('Tags', coerce=int) + + def set_choices(self): + self.tags.choices = [(t.id, t.text) for t in Tag.query.all()] + +class DeleteForm(FlaskForm): + pass \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/templates/edit.html b/Unit-02/04-flask-login/project/messages/templates/edit.html new file mode 100644 index 0000000..70b17c8 --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/templates/edit.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit Your Message to {{message.user.first_name}}

+
+ {{form.csrf_token}} + +

+ + {{form.content(placeholder="Edit Your Message to "+message.user.first_name)}} +
+ + {% if form.content.errors %} + {% for error in form.content.errors %} + {{ error }} + {% endfor %} + {% endif %} + +

+ + {{form.tags}} + +
+
+ +
+ {{form.csrf_token}} + +
+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/templates/index.html b/Unit-02/04-flask-login/project/messages/templates/index.html new file mode 100644 index 0000000..70f2e10 --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/templates/index.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} +

See All Messages for {{user.first_name}}

+ Send {{user.first_name}} a message +
+ + {% for message in user.messages %} +

+ {{message.content}} + + posted {{ moment(message.updated_on, local=True).fromNow() }} + {% for tag in message.tags %} +

+ {{tag.text}} +

+ {% endfor %} + + + Edit this message | + See More + + + +

+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/templates/new.html b/Unit-02/04-flask-login/project/messages/templates/new.html new file mode 100644 index 0000000..f6d701c --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/templates/new.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block content %} +

Leave a Message for {{user.first_name}}

+
+ {{form.csrf_token}} +

+ + {{form.content(placeholder="Limit to 280 characters")}} +
+ + {% if form.content.errors %} + {% for error in form.content.errors %} + {{ error }} + {% endfor %} + {% endif %} + + +

+ +

+ {{form.tags}} +

+ +
+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/templates/show.html b/Unit-02/04-flask-login/project/messages/templates/show.html new file mode 100644 index 0000000..956d9d4 --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/templates/show.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block content %} +

{{message.content}}

+ {% if message.tags|length == 0 %} +

{{message.content}} isn't tagged!

+ {% else %} + {% for tag in message.tags %} +

+ {{tag.text}} +

+ {% endfor %} + {% endif %} + Edit +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/messages/views.py b/Unit-02/04-flask-login/project/messages/views.py new file mode 100644 index 0000000..cf4002f --- /dev/null +++ b/Unit-02/04-flask-login/project/messages/views.py @@ -0,0 +1,81 @@ +from flask import Blueprint, render_template, redirect, request, url_for, flash +from project.models import User, Message, Tag +from project.messages.forms import MessageForm, DeleteForm +from project import db +from project.decorators import ensure_correct_user +from flask_login import login_required + + +messages_blueprint = Blueprint( + 'messages', + __name__, + template_folder = 'templates' +) + + + +@messages_blueprint.route('/', methods=["GET", "POST"]) +@login_required +def index(user_id): + user = User.query.get(user_id) + if request.method == "POST": + form = MessageForm(request.form) + form.set_choices() + if form.validate(): + new_message=Message(form.content.data, user.id) + for tag in form.tags.data: + new_message.tags.append(Tag.query.get(tag)) + db.session.add(new_message) + db.session.commit() + flash('Message Created!') + return redirect(url_for('messages.index', user_id=user.id)) + return render_template('new.html', user=user, form=form) + return render_template('index.html', user=user) + +@messages_blueprint.route('/new') +@login_required +@ensure_correct_user +def new(user_id): + form = MessageForm() + form.set_choices() + return render_template('new.html', user=User.query.get(user_id), form=form) + +@messages_blueprint.route('//edit') +@login_required +@ensure_correct_user +def edit(user_id, id): + message=Message.query.get(id) + tags = [tag.id for tag in message.tags] + form = MessageForm(tags=tags) + form.set_choices() + form.content.data = message.content + return render_template('edit.html', message=message, form=form) + +@messages_blueprint.route('/', methods=["GET", "PATCH", "DELETE"]) +@login_required +@ensure_correct_user +def show(user_id, id): + message=Message.query.get(id) + if request.method == b'PATCH': + form = MessageForm(request.form) + form.set_choices() + if form.validate(): + time = db.func.now() + message.updated_on = time + message.content = request.form['content'] + message.tags=[] + for tag in form.tags.data: + message.tags.append(Tag.query.get(tag)) + db.session.add(message) + db.session.commit() + flash('Message Updated!') + return redirect(url_for('messages.index', user_id=message.user.id)) + return render_template('edit.html', message=message, form=form) + if request.method == b'DELETE': + delete_form = DeleteForm(request.form) + if delete_form.validate(): + db.session.delete(message) + db.session.commit() + flash('Message Deleted!') + return redirect(url_for('messages.index', user_id=user_id)) + return render_template('show.html', message=message) \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/models.py b/Unit-02/04-flask-login/project/models.py new file mode 100644 index 0000000..18ebe65 --- /dev/null +++ b/Unit-02/04-flask-login/project/models.py @@ -0,0 +1,64 @@ +from project import db, bcrypt +from flask_login import UserMixin + +class User(db.Model, UserMixin): + + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.Text, unique=True) + password = db.Column(db.Text) + first_name = db.Column(db.Text) + last_name = db.Column(db.Text) + created_on = db.Column(db.DateTime, server_default=db.func.now()) + updated_on = db.Column(db.DateTime, server_default=db.func.now(), server_onupdate=db.func.now()) + messages = db.relationship('Message', backref='user', lazy='dynamic', cascade='all, delete') + + def __init__(self, first_name, last_name, username, password): + self.first_name = first_name + self.last_name = last_name + self.username = username + self.password = bcrypt.generate_password_hash(password).decode('UTF-8') + + def __repr__(self): + return f"The user's name is {self.first_name} {self.last_name}" + + @classmethod + def authenticate(cls, username, password): + found_user = cls.query.filter_by(username = username).first() + if found_user: + authenticated_user = bcrypt.check_password_hash(found_user.password, password) + if authenticated_user: + return found_user + return False + +MessageTag = db.Table('message_tags', + db.Column('message_id', db.Integer, db.ForeignKey('messages.id')), + db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')) +) + +class Message(db.Model): + + __tablename__ = 'messages' + + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text) + created_on = db.Column(db.DateTime, server_default=db.func.now()) + updated_on = db.Column(db.DateTime, server_default=db.func.now(), server_onupdate=db.func.now()) + user_id = db.Column(db.Integer, db.ForeignKey('users.id')) + tags = db.relationship('Tag', secondary=MessageTag, backref=db.backref('messages')) + + def __init__(self, content, user_id): + self.content = content + self.user_id = user_id + + +class Tag(db.Model): + + __tablename__ = 'tags' + + id = db.Column(db.Integer, primary_key=True) + text= db.Column(db.Text) + + def __init__(self, text): + self.text = text \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/forms.py b/Unit-02/04-flask-login/project/tags/forms.py new file mode 100644 index 0000000..e8a4afd --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/forms.py @@ -0,0 +1,18 @@ +from flask_wtf import FlaskForm +from wtforms import TextField, SelectMultipleField, widgets +from wtforms.validators import DataRequired +from project.models import Message + +class MultiCheckboxField(SelectMultipleField): + widget = widgets.ListWidget(prefix_label=False) + option_widget = widgets.CheckboxInput() + +class TagForm(FlaskForm): + text = TextField('Text', validators=[DataRequired()], render_kw={"class":"text-center col-3"}) + messages = MultiCheckboxField('Messages', coerce=int) + + def set_choices(self): + self.messages.choices = [(m.id, m.content) for m in Message.query.all()] + +class DeleteForm(FlaskForm): + pass \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/templates/tags/edit.html b/Unit-02/04-flask-login/project/tags/templates/tags/edit.html new file mode 100644 index 0000000..e3318f8 --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/templates/tags/edit.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit the {{tag.text}} Tag

+
+ {{ form.csrf_token }} +

+ + {{form.text(value=tag.text)}} + + {% if form.text.errors %} + {% for error in form.text.errors %} + {{error}} + {% endfor %} + {% endif %} + +

+

+ {{form.messages}} +

+ +
+ +

+
+ {{ form.csrf_token }} + +
+

+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/templates/tags/index.html b/Unit-02/04-flask-login/project/tags/templates/tags/index.html new file mode 100644 index 0000000..3c22d46 --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/templates/tags/index.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

All Tags

+ {% for tag in tags %} +

+ {{ tag.text }} +
+ Edit | See More +

+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/templates/tags/new.html b/Unit-02/04-flask-login/project/tags/templates/tags/new.html new file mode 100644 index 0000000..955caeb --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/templates/tags/new.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block content %} +

Add a New Tag

+
+ {{ form.csrf_token }} +

+ + {{form.text(placeholder=form.text.label.text)}} +

+

+ {{form.messages}} +

+ + +
+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/templates/tags/show.html b/Unit-02/04-flask-login/project/tags/templates/tags/show.html new file mode 100644 index 0000000..a1d1032 --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/templates/tags/show.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block content %} +

Messages with the "{{tag.text}}"" Tag

+{% if tag.messages|length == 0 %} +

No messages yet

+{% else %} + {% for m in tag.messages %} +

+ {{m.content}} +

+ {% endfor %} +{% endif %} +Edit +
+ {{ form.csrf_token }} + +
+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/tags/views.py b/Unit-02/04-flask-login/project/tags/views.py new file mode 100644 index 0000000..6eeab9c --- /dev/null +++ b/Unit-02/04-flask-login/project/tags/views.py @@ -0,0 +1,79 @@ +from flask import redirect, render_template, request, url_for, flash, Blueprint +from project.models import Message, Tag +from project.tags.forms import DeleteForm, TagForm +from project import db +from project.decorators import ensure_correct_user +from flask_login import login_required + + + +tags_blueprint = Blueprint( + 'tags', + __name__, + template_folder='templates' +) + + + +@tags_blueprint.route('/', methods=["GET", "POST"]) +@login_required +def index(): + if request.method == "POST": + form = TagForm(request.form) + form.set_choices() + if form.validate(): + tag = Tag(form.text.data) + for message in form.messages.data: + tag.messages.append(Message.query.get(message)) + db.session.add(tag) + db.session.commit() + flash('Tag Created!') + else: + return redirect(url_for('tags.new.html', form=form)) + return render_template('tags/index.html', tags=Tag.query.all()) + +@tags_blueprint.route('/new') +@login_required +def new(): + form = TagForm() + form.set_choices() + return render_template('tags/new.html', form=form) + +@tags_blueprint.route('//edit') +@login_required +@ensure_correct_user +def edit(id): + tag = Tag.query.get_or_404(id) + messages = [message.id for message in tag.messages] + form = TagForm(messages=messages) + form.set_choices() + delete_form = DeleteForm() + return render_template('tags/edit.html', tag=tag, form=form, delete_form=delete_form) + +@tags_blueprint.route('/', methods=["GET", "PATCH", "DELETE"]) +@login_required +def show(id): + tag = Tag.query.get(id) + delete_form = DeleteForm() + if request.method == b"PATCH": + form = TagForm(request.form) + form.set_choices() + if form.validate(): + tag.text = form.text.data + tag.messages = [] + for message in form.messages.data: + tag.messages.append(Message.query.get(message)) + db.session.add(tag) + db.session.commit() + flash('Tag Updated!') + return redirect(url_for('tags.index')) + else: + return render_template('tags/edit.html', form=form, tag=tag, delete_form=delete_form) + if request.method == b"DELETE": + delete_form = DeleteForm(request.form) + if delete_form.validate(): + db.session.delete(tag) + db.session.commit() + flash('Tag Deleted!') + return redirect(url_for('tags.index')) + return render_template('tags/show.html', tag=tag, form=delete_form) \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/templates/base.html b/Unit-02/04-flask-login/project/templates/base.html new file mode 100644 index 0000000..a029487 --- /dev/null +++ b/Unit-02/04-flask-login/project/templates/base.html @@ -0,0 +1,50 @@ + + + + + user-messages-login + + {{ moment.include_moment() }} + + + + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +

{{message}}

+ {% endfor %} + {% endif %} + {% endwith %} + + + {% block content %} + {% endblock %} + + + + + + + + \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/templates/errors.html b/Unit-02/04-flask-login/project/templates/errors.html new file mode 100644 index 0000000..a3eb234 --- /dev/null +++ b/Unit-02/04-flask-login/project/templates/errors.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} + +{% block content %} +

{{error}}

+ +

via GIPHY

+ +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/users/forms.py b/Unit-02/04-flask-login/project/users/forms.py new file mode 100644 index 0000000..860058b --- /dev/null +++ b/Unit-02/04-flask-login/project/users/forms.py @@ -0,0 +1,16 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, validators + +class UserForm(FlaskForm): + first_name=StringField('First Name', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + last_name=StringField('Last Name', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + username=StringField('Username', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + password=PasswordField('Password', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + +class LoginForm(FlaskForm): + username=StringField('Username', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + password=PasswordField('Password', [validators.DataRequired()],render_kw={"class":"col-4 text-center"}) + + +class DeleteForm(FlaskForm): + pass \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/users/templates/users/edit.html b/Unit-02/04-flask-login/project/users/templates/users/edit.html new file mode 100644 index 0000000..bc652d3 --- /dev/null +++ b/Unit-02/04-flask-login/project/users/templates/users/edit.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit {{current_user.username}}

+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +

+ + {{field(placeholder=field.label.text)}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +

+ {% endfor %} + +
+{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/users/templates/users/index.html b/Unit-02/04-flask-login/project/users/templates/users/index.html new file mode 100644 index 0000000..479f6e6 --- /dev/null +++ b/Unit-02/04-flask-login/project/users/templates/users/index.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block content %} + +

All Users

+

Welcome back {{current_user.username}}

+ {% for user in users %} +

+ {{user.first_name}} {{user.last_name}} member since {{ moment(user.created_on, local=True).fromNow() }} +
+ Edit {{user.first_name}} | + See {{user.first_name}}'s messages +

+ {{ delete_form.hidden_tag() }} + + +
+
+

+ {% endfor %} + +{% endblock %} diff --git a/Unit-02/04-flask-login/project/users/templates/users/login.html b/Unit-02/04-flask-login/project/users/templates/users/login.html new file mode 100644 index 0000000..197e24d --- /dev/null +++ b/Unit-02/04-flask-login/project/users/templates/users/login.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} +

Login!

+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +

+ {{field.label}} + {{field}} + + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +

+ {% endfor %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/users/templates/users/show.html b/Unit-02/04-flask-login/project/users/templates/users/show.html new file mode 100644 index 0000000..e69de29 diff --git a/Unit-02/04-flask-login/project/users/templates/users/signup.html b/Unit-02/04-flask-login/project/users/templates/users/signup.html new file mode 100644 index 0000000..7695779 --- /dev/null +++ b/Unit-02/04-flask-login/project/users/templates/users/signup.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} +

New User Sign-Up

+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +

+ + {{field(placeholder=field.label.text)}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +

+ {% endfor %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/Unit-02/04-flask-login/project/users/views.py b/Unit-02/04-flask-login/project/users/views.py new file mode 100644 index 0000000..1785357 --- /dev/null +++ b/Unit-02/04-flask-login/project/users/views.py @@ -0,0 +1,109 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from project.users.forms import UserForm, LoginForm, DeleteForm +from project import db, bcrypt +from project.models import User +from sqlalchemy.exc import IntegrityError +from project.decorators import prevent_login_signup, ensure_correct_user +from flask_login import login_user, logout_user, login_required + + +users_blueprint = Blueprint( + 'users', + __name__, + template_folder = 'templates' +) + +@login_required +@users_blueprint.route('/') +def index(): + delete_form = DeleteForm() + return render_template('users/index.html', users=User.query.all(), delete_form=delete_form) + + +@users_blueprint.route('/signup', methods=["GET","POST"]) +@prevent_login_signup +def signup(): + form = UserForm(request.form) + if form.validate(): + try: + new_user = User( + form.first_name.data, + form.last_name.data, + form.username.data, + form.password.data) + db.session.add(new_user) + db.session.commit() + login_user(new_user) + flash('User Created!') + return redirect(url_for('users.index')) + except IntegrityError as e: + flash('Username already taken') + return render_template('users/signup.html', form=form) + return render_template('users/signup.html', form=form) + + +@users_blueprint.route('/login', methods=["GET", "POST"]) +@prevent_login_signup +def login(): + form = LoginForm(request.form) + if request.method == "POST": + if form.validate(): + authenticated_user = User.authenticate(form.username.data, form.password.data) + if authenticated_user: + login_user(authenticated_user) + flash("You are now logged in!") + return redirect(url_for('users.index')) + else: + flash('Invalid Credentials') + return redirect(url_for('users.login')) + return render_template('users/login.html', form=form) + +# @users_blueprint.route('/signup') +# @prevent_login_signup +# def new(): +# user_form = UserForm() +# return render_template('users/signup.html', form=user_form) + +@users_blueprint.route('//edit') +@login_required +@ensure_correct_user +def edit(id): + found_user = User.query.get(id) + user_form = UserForm(obj=found_user) + return render_template('users/edit.html', user=found_user, form=user_form) + +@users_blueprint.route('/', methods=["GET", "PATCH", "DELETE"]) +@login_required +@ensure_correct_user +def show(id): + found_user = User.query.get(id) + if request.method == b"PATCH": + form = UserForm(request.form) + if form.validate(): + found_user.first_name = form.first_name.data + found_user.last_name = form.last_name.data + found_user.username = form.username.data + found_user.password = bcrypt.generate_password_hash(form.password.data).decode('UTF-8') + time = db.func.now() + found_user.updated_on = time + db.session.add(found_user) + db.session.commit() + flash('User Updated!') + return redirect(url_for('users.index')) + return render_template('users/edit.html', user=found_user, form=form) + if request.method ==b"DELETE": + delete_form = DeleteForm(request.form) + if delete_form.validate(): + db.session.delete(found_user) + db.session.commit() + logout_user() + flash('User Deleted!') + return redirect(url_for('users.login')) + return render_template('users/show.html', user=found_user) + +@users_blueprint.route('/logout') +@login_required +def logout(): + logout_user() + flash('Logged Out!') + return redirect(url_for('users.login')) \ No newline at end of file diff --git a/Unit-02/04-flask-login/requirements.txt b/Unit-02/04-flask-login/requirements.txt new file mode 100644 index 0000000..9beaf71 --- /dev/null +++ b/Unit-02/04-flask-login/requirements.txt @@ -0,0 +1,41 @@ +alembic==0.9.6 +appnope==0.1.0 +bcrypt==3.1.4 +cffi==1.11.2 +click==6.7 +decorator==4.1.2 +Flask==0.12.2 +Flask-Bcrypt==0.7.1 +Flask-Login==0.4.1 +Flask-Migrate==2.1.1 +Flask-Modus==0.0.1 +Flask-Moment==0.5.2 +Flask-Script==2.0.6 +Flask-SQLAlchemy==2.3.2 +Flask-Testing==0.6.2 +Flask-WTF==0.14.2 +gunicorn==19.7.1 +ipython==6.2.1 +ipython-genutils==0.2.0 +itsdangerous==0.24 +jedi==0.11.0 +Jinja2==2.10 +Mako==1.0.7 +MarkupSafe==1.0 +parso==0.1.0 +pexpect==4.3.0 +pickleshare==0.7.4 +prompt-toolkit==1.0.15 +psycopg2==2.7.3.2 +ptyprocess==0.5.2 +pycparser==2.18 +Pygments==2.2.0 +python-dateutil==2.6.1 +python-editor==1.0.3 +simplegeneric==0.8.1 +six==1.11.0 +SQLAlchemy==1.1.15 +traitlets==4.3.2 +wcwidth==0.1.7 +Werkzeug==0.12.2 +WTForms==2.1 From 2c5dbf5af08f1f133d1bc26d7e4514d8f0a4a723 Mon Sep 17 00:00:00 2001 From: Stephen Carrera Date: Wed, 6 Dec 2017 20:14:41 -0800 Subject: [PATCH 2/2] UI changes and auth adjustments --- Unit-02/04-flask-login/project/__init__.py | 2 ++ .../04-flask-login/project/messages/views.py | 1 - .../04-flask-login/project/static/style.css | 3 +++ .../project/tags/templates/tags/edit.html | 2 +- .../project/templates/base.html | 16 +++++++++++--- .../project/users/templates/users/edit.html | 5 +++++ .../project/users/templates/users/index.html | 21 ++++++++++++++----- Unit-02/04-flask-login/project/users/views.py | 9 ++++---- Unit-02/04-flask-login/requirements.txt | 8 +++++++ 9 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 Unit-02/04-flask-login/project/static/style.css diff --git a/Unit-02/04-flask-login/project/__init__.py b/Unit-02/04-flask-login/project/__init__.py index cffa408..0f43cbc 100644 --- a/Unit-02/04-flask-login/project/__init__.py +++ b/Unit-02/04-flask-login/project/__init__.py @@ -4,6 +4,8 @@ from flask_moment import Moment from flask_bcrypt import Bcrypt from flask_login import LoginManager + + import os diff --git a/Unit-02/04-flask-login/project/messages/views.py b/Unit-02/04-flask-login/project/messages/views.py index cf4002f..07278b6 100644 --- a/Unit-02/04-flask-login/project/messages/views.py +++ b/Unit-02/04-flask-login/project/messages/views.py @@ -34,7 +34,6 @@ def index(user_id): @messages_blueprint.route('/new') @login_required -@ensure_correct_user def new(user_id): form = MessageForm() form.set_choices() diff --git a/Unit-02/04-flask-login/project/static/style.css b/Unit-02/04-flask-login/project/static/style.css new file mode 100644 index 0000000..b511158 --- /dev/null +++ b/Unit-02/04-flask-login/project/static/style.css @@ -0,0 +1,3 @@ +ul { + list-style: none; +} diff --git a/Unit-02/04-flask-login/project/tags/templates/tags/edit.html b/Unit-02/04-flask-login/project/tags/templates/tags/edit.html index e3318f8..d285740 100644 --- a/Unit-02/04-flask-login/project/tags/templates/tags/edit.html +++ b/Unit-02/04-flask-login/project/tags/templates/tags/edit.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block content %} -

Edit the {{tag.text}} Tag

+

Edit the "{{tag.text}}" Tag

{{ form.csrf_token }}

diff --git a/Unit-02/04-flask-login/project/templates/base.html b/Unit-02/04-flask-login/project/templates/base.html index a029487..0cfe30a 100644 --- a/Unit-02/04-flask-login/project/templates/base.html +++ b/Unit-02/04-flask-login/project/templates/base.html @@ -5,9 +5,10 @@ user-messages-login {{ moment.include_moment() }} + -