diff --git a/Unit-02/03-hashing-sessions/app.py b/Unit-02/03-hashing-sessions/app.py new file mode 100644 index 0000000..7e10d6e --- /dev/null +++ b/Unit-02/03-hashing-sessions/app.py @@ -0,0 +1,4 @@ +from project import app + +if __name__ == '__main__': + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/manage.py b/Unit-02/03-hashing-sessions/manage.py new file mode 100644 index 0000000..e33b328 --- /dev/null +++ b/Unit-02/03-hashing-sessions/manage.py @@ -0,0 +1,10 @@ +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/03-hashing-sessions/migrations/README b/Unit-02/03-hashing-sessions/migrations/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/migrations/alembic.ini b/Unit-02/03-hashing-sessions/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/Unit-02/03-hashing-sessions/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/03-hashing-sessions/migrations/env.py b/Unit-02/03-hashing-sessions/migrations/env.py new file mode 100755 index 0000000..23663ff --- /dev/null +++ b/Unit-02/03-hashing-sessions/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/03-hashing-sessions/migrations/script.py.mako b/Unit-02/03-hashing-sessions/migrations/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/Unit-02/03-hashing-sessions/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/03-hashing-sessions/migrations/versions/02c5024ca366_adding_messages_table.py b/Unit-02/03-hashing-sessions/migrations/versions/02c5024ca366_adding_messages_table.py new file mode 100644 index 0000000..9ab3c44 --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/versions/02c5024ca366_adding_messages_table.py @@ -0,0 +1,34 @@ +"""adding messages table + +Revision ID: 02c5024ca366 +Revises: a40d037bd01f +Create Date: 2017-11-30 19:42:43.100357 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '02c5024ca366' +down_revision = 'a40d037bd01f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('messages', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('content', sa.Text(), 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') + # ### end Alembic commands ### diff --git a/Unit-02/03-hashing-sessions/migrations/versions/679a839ba586_.py b/Unit-02/03-hashing-sessions/migrations/versions/679a839ba586_.py new file mode 100644 index 0000000..1ef4fac --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/versions/679a839ba586_.py @@ -0,0 +1,32 @@ +"""Add password and username + +Revision ID: 679a839ba586 +Revises: a35b193c677c +Create Date: 2017-12-06 20:20:20.059722 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '679a839ba586' +down_revision = 'a35b193c677c' +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/03-hashing-sessions/migrations/versions/6f4cab9be9f4_adding_profile_link.py b/Unit-02/03-hashing-sessions/migrations/versions/6f4cab9be9f4_adding_profile_link.py new file mode 100644 index 0000000..9dbb957 --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/versions/6f4cab9be9f4_adding_profile_link.py @@ -0,0 +1,28 @@ +"""adding profile link + +Revision ID: 6f4cab9be9f4 +Revises: 02c5024ca366 +Create Date: 2017-12-03 20:54:31.115296 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6f4cab9be9f4' +down_revision = '02c5024ca366' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('profile_link', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'profile_link') + # ### end Alembic commands ### diff --git a/Unit-02/03-hashing-sessions/migrations/versions/a35b193c677c_add_many_to_many_relationship_with_.py b/Unit-02/03-hashing-sessions/migrations/versions/a35b193c677c_add_many_to_many_relationship_with_.py new file mode 100644 index 0000000..28dd37b --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/versions/a35b193c677c_add_many_to_many_relationship_with_.py @@ -0,0 +1,41 @@ +"""add many to many relationship with messages-tags + +Revision ID: a35b193c677c +Revises: 6f4cab9be9f4 +Create Date: 2017-12-05 17:26:14.653681 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a35b193c677c' +down_revision = '6f4cab9be9f4' +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('category', sa.Text(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('messages_tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('messages_id', sa.Integer(), nullable=True), + sa.Column('tags_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['messages_id'], ['messages.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['tags_id'], ['tags.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('messages_tags') + op.drop_table('tags') + # ### end Alembic commands ### diff --git a/Unit-02/03-hashing-sessions/migrations/versions/a40d037bd01f_adding_users_table.py b/Unit-02/03-hashing-sessions/migrations/versions/a40d037bd01f_adding_users_table.py new file mode 100644 index 0000000..1994d57 --- /dev/null +++ b/Unit-02/03-hashing-sessions/migrations/versions/a40d037bd01f_adding_users_table.py @@ -0,0 +1,33 @@ +"""adding users table + +Revision ID: a40d037bd01f +Revises: +Create Date: 2017-11-30 16:48:58.180330 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a40d037bd01f' +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.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users') + # ### end Alembic commands ### diff --git a/Unit-02/03-hashing-sessions/project/__init__.py b/Unit-02/03-hashing-sessions/project/__init__.py new file mode 100644 index 0000000..e521c30 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/__init__.py @@ -0,0 +1,35 @@ +from flask import Flask, redirect, url_for, render_template +from flask_sqlalchemy import SQLAlchemy +from flask_modus import Modus +from flask_bcrypt import Bcrypt +from os import environ + +app = Flask(__name__) +bcrypt = Bcrypt(app) +app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://localhost/users-messages' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['SECRET_KEY'] = environ.get('SECRET_KEY') +modus=Modus(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 +from project.messages.models import Message + +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') + +@app.route('/') +def root(): + return redirect(url_for('users.index')) + +@app.route('/messages') +def messages(): + messages = Message.query.all() + return render_template('messages.html', messages = messages) + +@app.errorhandler(404) +def page_not_found(e): + return render_template('users/404.html', e=e), 404 \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/decorators/__init__.py b/Unit-02/03-hashing-sessions/project/decorators/__init__.py new file mode 100644 index 0000000..f5c76e7 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/decorators/__init__.py @@ -0,0 +1,32 @@ +from functools import wraps +from flask import redirect, url_for, session, flash + +def ensure_authenticated(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + if not session.get('user_id'): + 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 session.get('user_id'): + 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): + # kwargs is a dictionary with user id as a key + correct_id = kwargs.get('user_id') or kwargs.get('id') + if correct_id != session.get('user_id'): + flash("Not Authorized") + return redirect(url_for('users.index',id=session.get('user_id'))) + return fn(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/messages/forms.py b/Unit-02/03-hashing-sessions/project/messages/forms.py new file mode 100644 index 0000000..342b510 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/forms.py @@ -0,0 +1,20 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SelectMultipleField, validators,widgets +from project.tags.models import Tag + +class MessageForm(FlaskForm): + content = StringField('Content', [validators.DataRequired()]) + + tags = SelectMultipleField( + 'Tags', + coerce=int, + widget=widgets.ListWidget(prefix_label=False), + option_widget=widgets.CheckboxInput() + ) + + def set_choices(self): + self.tags.choices = [(t.id, t.category) for t in Tag.query.all()] + + +class DeleteForm(FlaskForm): + pass \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/messages/models.py b/Unit-02/03-hashing-sessions/project/messages/models.py new file mode 100644 index 0000000..c198b31 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/models.py @@ -0,0 +1,21 @@ +from project import db + +MessageTag = db.Table('messages_tags', + db.Column('id', db.Integer, primary_key =True), + db.Column('messages_id', db.Integer, db.ForeignKey('messages.id', ondelete='cascade')), + db.Column('tags_id', db.Integer, db.ForeignKey('tags.id', ondelete='cascade')) + ) + +class Message(db.Model): + + __tablename__ = "messages" + + id = db.Column(db.Integer, primary_key=True) + content = db.Column(db.Text) + 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 + diff --git a/Unit-02/03-hashing-sessions/project/messages/templates/messages/edit.html b/Unit-02/03-hashing-sessions/project/messages/templates/messages/edit.html new file mode 100644 index 0000000..ee08ba6 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/templates/messages/edit.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit the message

+
+
+
+ {{ form.csrf_token }} +
+ {{form.content.label}} + {{form.content(value=message.content)}} + + {% if form.content.errors %} + {% for error in form.content.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+
+ {{form.tags(class_='form-control')}} +
+ +
+
+
+{% endblock %} + + + + diff --git a/Unit-02/03-hashing-sessions/project/messages/templates/messages/index.html b/Unit-02/03-hashing-sessions/project/messages/templates/messages/index.html new file mode 100644 index 0000000..b45cca2 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/templates/messages/index.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} +{% block content %} + +
+ + + +
+ Profile Link +
+
+ +
+
+

Messages for {{user.first_name}} {{user.last_name}}

+
+
+ +
+
+ {% for message in user.messages %} +
+ {{message.content}} +
+ {{delete_form.hidden_tag()}} + +
+
+
+
+ {% endfor %} +
+
+
+{% endblock %} + diff --git a/Unit-02/03-hashing-sessions/project/messages/templates/messages/new.html b/Unit-02/03-hashing-sessions/project/messages/templates/messages/new.html new file mode 100644 index 0000000..dfd4695 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/templates/messages/new.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +

Add a new message

+
+
+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{field.label}} + {{field(class_='form-control')}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+ {% endfor %} + +
+
+
+ + +{% endblock %} + diff --git a/Unit-02/03-hashing-sessions/project/messages/templates/messages/show.html b/Unit-02/03-hashing-sessions/project/messages/templates/messages/show.html new file mode 100644 index 0000000..e69de29 diff --git a/Unit-02/03-hashing-sessions/project/messages/views.py b/Unit-02/03-hashing-sessions/project/messages/views.py new file mode 100644 index 0000000..d1bc383 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/messages/views.py @@ -0,0 +1,79 @@ +from flask import redirect, render_template, request, url_for, Blueprint, flash +from project.messages.models import Message +from project.users.models import User +from project.tags.models import Tag +from project.messages.forms import MessageForm, DeleteForm +from project import db +from project.decorators import ensure_authenticated, ensure_correct_user + +messages_blueprint = Blueprint( + 'messages', + __name__, + template_folder = 'templates' +) + +# Messages +@messages_blueprint.route('/', methods = ["GET", "POST"]) +@ensure_authenticated +def index(user_id): + delete_form = DeleteForm() + if request.method == 'POST': + form = MessageForm(request.form) + form.set_choices() + if form.validate(): + new_message = Message(request.form.get('content'), 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!") + else: + return render_template('messages/new.html', user = User.query.get_or_404(user_id), form = form) + return render_template('messages/index.html', user=User.query.get_or_404(user_id), delete_form=delete_form) + +@messages_blueprint.route('/new') +@ensure_authenticated +@ensure_correct_user +def new(user_id): + message_form = MessageForm() + message_form.set_choices() + # pass in the user here cause need it for the post request + return render_template('messages/new.html', user = User.query.get_or_404(user_id), form = message_form) + +@messages_blueprint.route('//edit') +@ensure_authenticated +@ensure_correct_user +def edit(user_id, id): + found_message = Message.query.get_or_404(id) + tags = [tag.id for tag in found_message.tags] + message_form = MessageForm(tags=tags) + message_form.set_choices() + return render_template('messages/edit.html', message = found_message, form=message_form) + +@messages_blueprint.route('/', methods = ["GET", "PATCH", "DELETE"]) +@ensure_authenticated +@ensure_correct_user +def show(user_id, id): + found_message = Message.query.get_or_404(id) + if request.method == b'PATCH': + form = MessageForm(request.form) + form.set_choices() + if form.validate(): + found_message.content = request.form.get('content') + found_message.tags = [] + for tag in form.tags.data: + found_message.tags.append(Tag.query.get(tag)) + db.session.add(found_message) + db.session.commit() + flash("Message Updated!") + return redirect(url_for('messages.index', user_id = user_id)) + else: + return render_template('messages/edit.html', message = found_message, form=form) + if request.method == b'DELETE': + delete_form = DeleteForm(request.form) + if delete_form.validate(): + db.session.delete(found_message) + db.session.commit() + flash("Message Deleted!") + return redirect(url_for('messages.index', user_id = user_id)) + return render_template('messages/show.html', message=found_message) diff --git a/Unit-02/03-hashing-sessions/project/static/script.js b/Unit-02/03-hashing-sessions/project/static/script.js new file mode 100644 index 0000000..9dbeea9 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/static/script.js @@ -0,0 +1,18 @@ +$(document).ready(function(){ + function length_less_than_six(string){ + if(string.length<=6) return true + return false + } + + $("#password").on('keyup', function(e){ + var value = e.target.value; + var $inputId = `#${e.target.id}` + if (length_less_than_six(value)) { + console.log($inputId) + $($inputId).next().text("Make password longer than six characters") + } + else { + $($inputId).next().empty() + } + }) +}); diff --git a/Unit-02/03-hashing-sessions/project/static/style.css b/Unit-02/03-hashing-sessions/project/static/style.css new file mode 100644 index 0000000..8e96cdf --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/static/style.css @@ -0,0 +1,47 @@ +body { + background-color: #F0FFFF; +} + +h1 { + text-align: center; +} + +form, .list-group{ + width: 50%; +} + + +.jumbotron { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.3))), url('https://static.pexels.com/photos/106400/pexels-photo-106400.jpeg'); + background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.3)), url('https://static.pexels.com/photos/106400/pexels-photo-106400.jpeg'); + background-attachment:fixed; + background-size: cover; + background-position: center; + height: 80vh; +} + +.hero-text-box { + color: white; + text-align: center; + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + +.card { + padding-bottom: 20px; + margin-bottom: 20px; +} + +.error { + color: red; +} + +.profile_picture { + border-radius: 50%; + height: 300px; + width: auto; + display: block; +} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/tags/forms.py b/Unit-02/03-hashing-sessions/project/tags/forms.py new file mode 100644 index 0000000..2468293 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/forms.py @@ -0,0 +1,18 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SelectMultipleField, widgets, validators +from project.messages.models import Message + +class TagForm(FlaskForm): + category = StringField('Category', [validators.DataRequired()]) + + messages = SelectMultipleField( + 'Messages', + coerce=int, + widget=widgets.ListWidget(prefix_label=False), + option_widget=widgets.CheckboxInput()) + + 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/03-hashing-sessions/project/tags/models.py b/Unit-02/03-hashing-sessions/project/tags/models.py new file mode 100644 index 0000000..80ad2e6 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/models.py @@ -0,0 +1,10 @@ +from project import db + +class Tag(db.Model): + __tablename__ = 'tags' + + id = db.Column(db.Integer, primary_key=True) + category = db.Column(db.Text) + + def __init__(self, category): + self.category = category \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/tags/templates/tags/edit.html b/Unit-02/03-hashing-sessions/project/tags/templates/tags/edit.html new file mode 100644 index 0000000..a67ed72 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/templates/tags/edit.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit the tag

+
+
+
+ {{ form.csrf_token }} +
+ {{form.category.label}} + {{form.category(value=tag.category)}} + + {% if form.category.errors %} + {% for error in form.category.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+
+ {{form.messages(class_='form-control')}} +
+ +
+
+
+{% endblock %} + + diff --git a/Unit-02/03-hashing-sessions/project/tags/templates/tags/index.html b/Unit-02/03-hashing-sessions/project/tags/templates/tags/index.html new file mode 100644 index 0000000..eeae8b9 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/templates/tags/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block content %} +

All Tags

+
+ {% for tag in tags %} +

+ {{tag.category}} | See messages in category +

+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/tags/templates/tags/new.html b/Unit-02/03-hashing-sessions/project/tags/templates/tags/new.html new file mode 100644 index 0000000..6db2c13 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/templates/tags/new.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +

Add a new tag

+
+
+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{field.label}} + {{field(class_='form-control')}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+ {% endfor %} + +
+
+
+{% endblock %} + + + diff --git a/Unit-02/03-hashing-sessions/project/tags/templates/tags/show.html b/Unit-02/03-hashing-sessions/project/tags/templates/tags/show.html new file mode 100644 index 0000000..35fdfa7 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/templates/tags/show.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} +

Messages in {{tag.category}} category

+{% if tag.messages|length == 0 %} + No messages yet +{% else %} + {% for message in tag.messages %} +

+ {{message.content}} by {{message.user.first_name}} {{message.user.last_name}} +

+ {% endfor %} +{% endif %} + +     Edit tag    +
+ {{delete_form.hidden_tag()}} + +
+ + +{% endblock %} + diff --git a/Unit-02/03-hashing-sessions/project/tags/views.py b/Unit-02/03-hashing-sessions/project/tags/views.py new file mode 100644 index 0000000..733abe7 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/tags/views.py @@ -0,0 +1,70 @@ +from flask import redirect, render_template, request, url_for, Blueprint, flash +from project.tags.models import Tag +from project.messages.models import Message +from project.tags.forms import TagForm, DeleteForm +from project import db + +tags_blueprint = Blueprint( + 'tags', + __name__, + template_folder = 'templates' +) + + +@tags_blueprint.route('/', methods = ['GET', 'POST']) +def index(): + if request.method == "POST": + form = TagForm(request.form) + form.set_choices() + if form.validate_on_submit(): + new_tag = Tag(request.form.get('category')) + for message in form.messages.data: + new_tag.messages.append(Message.query.get(message)) + db.session.add(new_tag) + db.session.commit() + flash('Tag Created!') + return redirect(url_for('tags.index')) + else: + return render_template('tags/new.html', form = form) + return render_template('tags/index.html', tags = Tag.query.all()) + +@tags_blueprint.route('/new') +def new(): + tag_form = TagForm() + tag_form.set_choices() + return render_template('tags/new.html', form = tag_form) + +@tags_blueprint.route('//edit') +def edit(id): + found_tag = Tag.query.get_or_404(id) + messages = [message.id for message in found_tag.messages] + tag_form = TagForm(messages=messages) + tag_form.set_choices() + return render_template('tags/edit.html', tag = found_tag, form =tag_form) + +@tags_blueprint.route('/', methods = ['GET', 'PATCH', 'DELETE']) +def show(id): + delete_form = DeleteForm() + tag = Tag.query.get_or_404(id) + if request.method == b'PATCH': + form = TagForm(request.form) + form.set_choices() + if form.validate(): + tag.category = form.category.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.show', id=id)) + else: + return render_template('tags/edit.html', tag = tag, form =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, delete_form = delete_form) \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/templates/base.html b/Unit-02/03-hashing-sessions/project/templates/base.html new file mode 100644 index 0000000..49a8fca --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/templates/base.html @@ -0,0 +1,70 @@ + + + + + + User CRUD + + + + + + + + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + {% block content %} + + {% endblock %} + + + + + + + + + + + \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/templates/messages.html b/Unit-02/03-hashing-sessions/project/templates/messages.html new file mode 100644 index 0000000..27e24bb --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/templates/messages.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% block content %} + {% for message in messages %} +

{{ message.content }}

+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/forms.py b/Unit-02/03-hashing-sessions/project/users/forms.py new file mode 100644 index 0000000..30b6744 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/forms.py @@ -0,0 +1,17 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, validators + +class UserForm(FlaskForm): + first_name = StringField('First Name', [validators.DataRequired()]) + last_name = StringField('Last Name', [validators.DataRequired()]) + username = StringField('Username', [validators.DataRequired()]) + password = PasswordField('Password', [validators.DataRequired()]) + profile_link = StringField('Profile Link', [validators.DataRequired()]) + + +class LoginForm(FlaskForm): + username = StringField('Username', [validators.DataRequired()]) + password = PasswordField('Password', [validators.DataRequired()]) + +class DeleteForm(FlaskForm): + pass \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/models.py b/Unit-02/03-hashing-sessions/project/users/models.py new file mode 100644 index 0000000..c7386a6 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/models.py @@ -0,0 +1,30 @@ +from project import db, bcrypt + +class User(db.Model): + + __tablename__ = "users" + + # columns! + 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) + profile_link = db.Column(db.Text) + messages = db.relationship('Message', backref="user", lazy="dynamic", cascade="all,delete") + + def __init__(self, first_name, last_name, username, password, profile_link): + self.first_name = first_name + self.last_name = last_name + self.profile_link = profile_link + self.username = username + self.password = bcrypt.generate_password_hash(password).decode('UTF-8') + + @classmethod + def authenticate(cls, username, password): + found_user = cls.query.filter_by(username=username).first() + if found_user: + is_authenticated = bcrypt.check_password_hash(found_user.password, password) + if is_authenticated: + return found_user + return False \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/404.html b/Unit-02/03-hashing-sessions/project/users/templates/users/404.html new file mode 100644 index 0000000..99e5037 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/404.html @@ -0,0 +1,4 @@ +{% extends 'base.html' %} +{% block content %} + An error has occurred +{% endblock %} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/edit.html b/Unit-02/03-hashing-sessions/project/users/templates/users/edit.html new file mode 100644 index 0000000..686b70c --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/edit.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block content %} +

Edit user!

+
+
+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{field.label}} + {{field(class_='form-control')}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + + +
+ {% endfor %} + +
+
+ +
+{% endblock %} + diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/index.html b/Unit-02/03-hashing-sessions/project/users/templates/users/index.html new file mode 100644 index 0000000..dc45494 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/index.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% block content %} +
+
+
+

Super dope ass user/messages application

+

CRUD like a boss! CRUD Users and CRUD Messages. What more can you crudding want?

+

Welcome {{g.current_user.username}}

+
+

+ Add a new user! +

+
+
+
+ +
+

See All Our Users

+
+ {% for user in users %} +
+
+ Profile Link +
+

{{user.first_name}} {{user.last_name}}

+ See messages | + Edit user +
+
+ {{delete_form.hidden_tag()}} + +
+
+
+
+
+ {% endfor %} +
+
+ + +{% endblock %} + + diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/login.html b/Unit-02/03-hashing-sessions/project/users/templates/users/login.html new file mode 100644 index 0000000..ad10c47 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/login.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} +

Login!

+
+
+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{field.label}} + {{field(class_='form-control')}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+ {% endfor %} + +
+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/new.html b/Unit-02/03-hashing-sessions/project/users/templates/users/new.html new file mode 100644 index 0000000..b0f2bc4 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/new.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} +

Add a new user!

+
+
+
+ {{form.hidden_tag()}} + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{field.label}} + {{field(class_='form-control')}} + + {% if field.errors %} + {% for error in field.errors %} + {{ error }} + {% endfor %} + {% endif %} + +
+ {% endfor %} + +
+
+ +
+ +{% endblock %} diff --git a/Unit-02/03-hashing-sessions/project/users/templates/users/show.html b/Unit-02/03-hashing-sessions/project/users/templates/users/show.html new file mode 100644 index 0000000..3c7fe69 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/templates/users/show.html @@ -0,0 +1,5 @@ +{% extends 'base.html' %} + +{% block content %} + {{user.first_name}} {{user.last_name}} +{% endblock %} \ No newline at end of file diff --git a/Unit-02/03-hashing-sessions/project/users/views.py b/Unit-02/03-hashing-sessions/project/users/views.py new file mode 100644 index 0000000..7930cb6 --- /dev/null +++ b/Unit-02/03-hashing-sessions/project/users/views.py @@ -0,0 +1,114 @@ +from flask import redirect, render_template, request, url_for, Blueprint, flash, session, g +from project.users.models import User +from project.users.forms import UserForm, DeleteForm, LoginForm +from project import db, bcrypt +from sqlalchemy.exc import IntegrityError +from project.decorators import ensure_authenticated, prevent_login_signup, ensure_correct_user + +users_blueprint = Blueprint( + 'users', + __name__, + template_folder = 'templates' +) + +@users_blueprint.before_request +def current_user(): + if session.get('user_id'): + g.current_user = User.query.get(session['user_id']) + else: + g.current_user = None + + +@users_blueprint.route('/') +@ensure_authenticated +def index(): + delete_form = DeleteForm() + return render_template('users/index.html', users = User.query.all(), delete_form = delete_form) + +@users_blueprint.route('/', methods = ['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, + form.profile_link.data) + db.session.add(new_user) + db.session.commit() + session['user_id'] = new_user.id + flash('User Created!') + return redirect(url_for('users.index')) + except IntegrityError as e: + flash('Username already taken') + return render_template('users/new.html', form = form) + + return render_template('users/new.html', form = form) + + +@users_blueprint.route('/login', methods = ["GET", "POST"]) +@prevent_login_signup +def login(): + form = LoginForm(request.form) + if form.validate_on_submit(): + authenticated_user = User.authenticate(form.username.data, form.password.data) + if authenticated_user: + session['user_id'] = authenticated_user.id + 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('/new') +@prevent_login_signup +def new(): + user_form = UserForm() + return render_template('users/new.html', form = user_form) + +@users_blueprint.route('//edit') +@ensure_authenticated +@ensure_correct_user +def edit(id): + found_user = User.query.get_or_404(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']) +@ensure_authenticated +@ensure_correct_user +def show(id): + found_user = User.query.get_or_404(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.profile_link = form.profile_link.data + found_user.password = bcrypt.generate_password_hash(form.password.data).decode('UTF-8') + db.session.add(found_user) + db.session.commit() + flash('User Updated!') + return redirect(url_for('users.index')) + else: + 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() + flash('User Deleted!') + return redirect(url_for('users.index')) + return render_template('users/show.html', user = User.query.get(id)) + +@users_blueprint.route('/logout') +@ensure_authenticated +def logout(): + session.pop('user_id') + flash('Logged Out!') + return redirect(url_for('users.login')) +