From f9020e921fc5bb98328818e1c7e63d98d6342c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana?= Date: Sat, 3 Jun 2023 16:59:29 +0200 Subject: [PATCH] Chapter 4 --- .vscode/settings.json | 6 + app.db | Bin 0 -> 32768 bytes app/__init__.py | 15 +++ app/forms.py | 10 ++ app/models.py | 23 ++++ app/routes.py | 27 +++++ app/templates/base.html | 29 +++++ app/templates/index.html | 8 ++ app/templates/login.html | 24 ++++ config.py | 11 ++ microblog.py | 1 + migrations/README | 1 + migrations/alembic.ini | 50 ++++++++ migrations/env.py | 110 ++++++++++++++++++ migrations/script.py.mako | 24 ++++ .../versions/d56801750ac8_users_posts.py | 57 +++++++++ 16 files changed, 396 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 app.db create mode 100644 app/__init__.py create mode 100644 app/forms.py create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/templates/base.html create mode 100644 app/templates/index.html create mode 100644 app/templates/login.html create mode 100644 config.py create mode 100644 microblog.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/d56801750ac8_users_posts.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d99f2f3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/app.db b/app.db new file mode 100644 index 0000000000000000000000000000000000000000..84e8525eea7533553cd57982d5c53dc547c2f603 GIT binary patch literal 32768 zcmeI&&rj1}7{KwC9~&Ev8k1!Q6Ma1pS5f4*yJ(zHaB+0f4V6<hJ zfLBkPJ@5y3^guj%v~M@cHiKEbxR7tNZhhar&(o)$cRRf4iYs}q?uyOoLB*+yY2~J( zs>*XA6h#^K_aT43*dqSHz(vFVR9lX(IviHUH-99wUrO}GXGKeBUq-e^ZYRDbRKLlF z00IagfB*srAb=TXv7UBYUq}tJ`(2;@0X;Wly$-<}D*zGDOZ? zFy4tjhKPbGyAqx9ovk!UKJR8Q=>v%cfX({hhCGnKM0@XNZ!Si&rn zgt?l}KNRuUdbRvfyvbVgFSAy9a%Mu8o#7?Kf?rZ8w`|DX!;#< zj5U$=%0cjA!7_47rtC{MLv&#oi-u*G^F~p~zF=@%e>)gSJxZ#dE>wcy@@?0txbid7 zyp>fmx3bz|(>74j$PN))PKN5{KqPfPsaD$(yA{XVkpl<+mNgi4nKEeV$9MZ1()R*s znMCdbZPJYzu9TLiWpSbQ+)UurxL`O1Q;jLub858@)q}FV<AI_crYJ?m^9~?shc$+Uo8&Dbn#+GtAyStc2H|p3>XCIr9axShBMI zmr8VGu=ln%L|ch1`SiH{v@eoc8B>*Dc53@O{wm&f4(pBCvOAMac69XnKeAwLpYRV< z?Wf|u*bqPf0R#|0009ILKmY**5J2FX2#lzGqvhGT%*5oA*$HPO6Zh|n_5U@QTy6sa z1Q0*~0R#|0009ILKmdWJfYv`6YhC}#=l}kn{uS+1I|T^=1Q0*~0R#|0009ILKmY** zdR`#&FBQt?|H1nIOgsCRcw!Jh009ILKmY**5I_I{1Q0;rKMFkP?%Lt{|6DuoE`m`A zAbzjq(QNCXf-009ILKmY**5I_I{1Q6&!fjd{%^czRLCQ<+Z literal 0 HcmV?d00001 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e9c10e1 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,15 @@ +from flask import Flask +from config import Config +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +app = Flask(__name__) +app.config.from_object(Config) + +# Puestos fuera del tutorial +app.jinja_env.auto_reload = True +app.config["TEMPLATES_AUTO_RELOAD"] = True +db = SQLAlchemy(app) +migrate = Migrate(app, db) + +from app import routes, models diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..55a4d2d --- /dev/null +++ b/app/forms.py @@ -0,0 +1,10 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms.validators import DataRequired + + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember_me = BooleanField("Remember Me") + submit = SubmitField("Sign In") diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..1dffb58 --- /dev/null +++ b/app/models.py @@ -0,0 +1,23 @@ +from datetime import datetime +from app import db + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), index=True, unique=True) + email = db.Column(db.String(120), index=True, unique=True) + password_hash = db.Column(db.String(128)) + posts = db.relationship("Post", backref="author", lazy="dynamic") + + def __repr__(self): + return "".format(self.username) + + +class Post(db.Model): + id = db.Column(db.Integer, primary_key=True) + body = db.Column(db.String(140)) + timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) + user_id = db.Column(db.Integer, db.ForeignKey("user.id")) + + def __repr__(self): + return "".format(self.body) diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..f5b5e4f --- /dev/null +++ b/app/routes.py @@ -0,0 +1,27 @@ +from flask import render_template, flash, redirect, url_for +from app import app +from app.forms import LoginForm + + +@app.route("/") +@app.route("/index") +def index(): + user = {"username": "Miguel"} + posts = [ + {"author": {"username": "John"}, "body": "Beautiful day in Portland!"}, + {"author": {"username": "Susan"}, "body": "The Avengers movie was so cool!"}, + ] + return render_template("index.html", title="Home", user=user, posts=posts) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + form = LoginForm() + if form.validate_on_submit(): + flash( + "Login requested for user {}, remember_me={}".format( + form.username.data, form.remember_me.data + ) + ) + return redirect(url_for("index")) + return render_template("login.html", title="Sign In", form=form) diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..917a93a --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,29 @@ + + + {% if title %} + {{ title }} - microblog + {% else %} + microblog + {% endif %} + + +
+ Microblog: + Home + Login +
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %} + + {% endblock %} + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..40f912e --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +

Hi, {{ user.username }}!

+ {% for post in posts %} +

{{ post.author.username }} says: {{ post.body }}

+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..68103f2 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block content %} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size = 32) }}
+ {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size = 32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+
+{% endblock %} diff --git a/config.py b/config.py new file mode 100644 index 0000000..694181f --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Config(object): + SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess" + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL" + ) or "sqlite:///" + os.path.join(basedir, "app.db") + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/microblog.py b/microblog.py new file mode 100644 index 0000000..e524e69 --- /dev/null +++ b/microblog.py @@ -0,0 +1 @@ +from app import app \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# 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,flask_migrate + +[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 + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[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/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..89f80b2 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,110 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# 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') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except TypeError: + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# 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 get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +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, target_metadata=get_metadata(), literal_binds=True + ) + + 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.') + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/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/migrations/versions/d56801750ac8_users_posts.py b/migrations/versions/d56801750ac8_users_posts.py new file mode 100644 index 0000000..88ffe22 --- /dev/null +++ b/migrations/versions/d56801750ac8_users_posts.py @@ -0,0 +1,57 @@ +"""users posts + +Revision ID: d56801750ac8 +Revises: +Create Date: 2023-06-03 16:52:06.559482 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd56801750ac8' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=True), + sa.Column('email', sa.String(length=120), nullable=True), + sa.Column('password_hash', sa.String(length=128), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True) + batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True) + + op.create_table('post', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('body', sa.String(length=140), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_post_timestamp'), ['timestamp'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('post', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_post_timestamp')) + + op.drop_table('post') + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_username')) + batch_op.drop_index(batch_op.f('ix_user_email')) + + op.drop_table('user') + # ### end Alembic commands ###