From: Michael Rasmussen Date: Wed, 29 Nov 2017 03:12:38 +0000 (+0100) Subject: First half X-Git-Url: http://git.datanom.net/flask-test.git/commitdiff_plain/db4f0ba9d4aec596f179335f1d86b3dd2bb206c6 First half Signed-off-by: Michael Rasmussen --- diff --git a/.gitignore b/.gitignore index e9bd007..013141f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ flask/ +db_repository/ +app.db +tmp/ +__pycache__/ app/__pycache__/ diff --git a/app/__init__.py b/app/__init__.py index 3ab3eef..8cfa1fb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,38 @@ +import os from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager +from flask_openid import OpenID +from config import basedir, ADMINS, MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD app = Flask(__name__) -from app import views +app.config.from_object('config') +db = SQLAlchemy(app) + +lm = LoginManager() +lm.init_app(app) +lm.login_view = 'login' +oid = OpenID(app, os.path.join(basedir, 'tmp')) + +if not app.debug: + import logging + from logging.handlers import SMTPHandler + credentials = None + if MAIL_USERNAME or MAIL_PASSWORD: + credentials = (MAIL_USERNAME, MAIL_PASSWORD) + mail_handler = SMTPHandler((MAIL_SERVER, MAIL_PORT), 'no-reply@' + MAIL_SERVER, ADMINS, 'WPP failure', credentials) + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + +if not app.debug: + import logging + from logging.handlers import RotatingFileHandler + file_handler = RotatingFileHandler('tmp/wpp.log', 'a', 1 * 1024 * 1024, 10) + file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) + app.logger.setLevel(logging.INFO) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + app.logger.info('wpp startup') + +from app import views, models diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..8c0d16c --- /dev/null +++ b/app/forms.py @@ -0,0 +1,27 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, BooleanField, TextAreaField +from wtforms.validators import DataRequired, Length + +class LoginForm(FlaskForm): + openid = StringField('openid', validators=[DataRequired()]) + remember_me = BooleanField('remember_me', default=False) + +class EditForm(FlaskForm): + nickname = StringField('nickname', validators=[DataRequired()]) + about_me = TextAreaField('about_me', validators=[Length(min=0, max=140)]) + + def __init__(self, original_nickname, *args, **kwargs): + FlaskForm.__init__(self, *args, **kwargs) + self.original_nickname = original_nickname + + def validate(self): + if not FlaskForm.validate(self): + return False + if self.nickname.data == self.original_nickname: + return True + user = User.query.filter_by(nickname=self.nickname.data).first() + if user != None: + self.nickname.errors.append('This nickname is already in use. Please choose another one.') + return False + return True + diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..6013939 --- /dev/null +++ b/app/models.py @@ -0,0 +1,56 @@ +from app import db +from hashlib import md5 + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + nickname = db.Column(db.String(64), index=True, unique=True) + email = db.Column(db.String(120), index=True, unique=True) + posts = db.relationship('Post', backref='author', lazy='dynamic') + about_me = db.Column(db.String(140)) + last_seen = db.Column(db.DateTime) + + @property + def is_authenticated(self): + return True + + @property + def is_active(self): + return True + + @property + def is_anonymous(self): + return False + + def get_id(self): + try: + return unicode(self.id) # python 2 + except NameError: + return str(self.id) # python 3 + + def avatar(self, size): + return 'http://www.gravatar.com/avatar/%s?d=mm&s=%d' % (md5(self.email.encode('utf-8')).hexdigest(), size) + + @staticmethod + def make_unique_nickname(nickname): + if User.query.filter_by(nickname=nickname).first() is None: + return nickname + version = 2 + while True: + new_nickname = nickname + str(version) + if User.query.filter_by(nickname=new_nickname).first() is None: + break + version += 1 + return new_nickname + + def __repr__(self): + return '' % (self.nickname) + +class Post(db.Model): + id = db.Column(db.Integer, primary_key = True) + body = db.Column(db.String(140)) + timestamp = db.Column(db.DateTime) + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + + def __repr__(self): + return '' % (self.body) + diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..1a02eee --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1,7 @@ + +{% extends "base.html" %} + +{% block content %} +

File Not Found

+

Back

+{% endblock %} diff --git a/app/templates/500.html b/app/templates/500.html new file mode 100644 index 0000000..bda4fd0 --- /dev/null +++ b/app/templates/500.html @@ -0,0 +1,8 @@ + +{% extends "base.html" %} + +{% block content %} +

An unexpected error has occurred

+

The administrator has been notified. Sorry for the inconvenience!

+

Back

+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..5a4dcdc --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,29 @@ + + + {% if title %} + {{ title }} - Web Photo Portfolio + {% else %} + Welcome to Web Photo Portfolio + {% endif %} + + +
WPP: Home + {% if g.user.is_authenticated %} + | Your Profile + | Logout + {% endif %} +
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} + + + diff --git a/app/templates/edit.html b/app/templates/edit.html new file mode 100644 index 0000000..5e3c460 --- /dev/null +++ b/app/templates/edit.html @@ -0,0 +1,29 @@ + +{% extends "base.html" %} + +{% block content %} +

Edit Your Profile

+
+ {{form.hidden_tag()}} + + + + + + + + + + + + + +
Your nickname: + {{ form.nickname(size=24) }} + {% for error in form.errors.nickname %} +
[{{ error }}] + {% endfor %} +
About yourself:{{ form.about_me(cols=32, rows=4) }}
+
+{% endblock %} + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..80a6eb3 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% block content %} +

Hi, {{ user.nickname }}!

+ {% for post in posts %} +

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

+ {% endfor %} +{% endblock %} + diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..f28940b --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,36 @@ + +{% extends "base.html" %} + +{% block content %} + +

Sign In

+

{{ error }}

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

+ Please enter your OpenID:
+ {{ form.openid(size=80) }}
+ {% for error in form.openid.errors %} + [{{ error }}] + {% endfor %}
+ |{% for pr in providers %} + {{ pr.name }} | + {% endfor %} +

+

{{ form.remember_me }} Remember Me

+

+

+
+{% endblock %} diff --git a/app/templates/post.html b/app/templates/post.html new file mode 100644 index 0000000..d55ba60 --- /dev/null +++ b/app/templates/post.html @@ -0,0 +1,5 @@ + + + + +
{{ post.author.nickname }} says:
{{ post.body }}
diff --git a/app/templates/user.html b/app/templates/user.html new file mode 100644 index 0000000..7e76e61 --- /dev/null +++ b/app/templates/user.html @@ -0,0 +1,26 @@ + +{% extends "base.html" %} + +{% block content %} + + + + + +
+

User: {{ user.nickname }}

+ {% if user.about_me %} +

{{ user.about_me }}

+ {% endif %} + {% if user.last_seen %} +

Last seen on: {{ user.last_seen }}

+ {% endif %} + {% if user.id == g.user.id %} +

Edit

+ {% endif %} +
+
+ {% for post in posts %} + {% include 'post.html' %} + {% endfor %} +{% endblock %} diff --git a/app/views.py b/app/views.py index d467b15..e00bb0d 100644 --- a/app/views.py +++ b/app/views.py @@ -1,6 +1,129 @@ -from app import app +from flask import render_template, flash, redirect, session, url_for, request, g +from flask_login import login_user, logout_user, current_user, login_required +from datetime import datetime +from app import app, db, lm, oid +from .forms import LoginForm, EditForm +from .models import User + + +@lm.user_loader +def load_user(id): + return User.query.get(int(id)) + + +@app.before_request +def before_request(): + g.user = current_user + if g.user.is_authenticated: + g.user.last_seen = datetime.utcnow() + db.session.add(g.user) + db.session.commit() + + +@app.errorhandler(404) +def not_found_error(error): + return render_template('404.html'), 404 + + +@app.errorhandler(500) +def internal_error(error): + db.session.rollback() + return render_template('500.html'), 500 + @app.route('/') @app.route('/index') +@login_required def index(): - return "Hello, World!" + user = g.user + posts = [ + { + 'author': {'nickname': 'John'}, + 'body': 'Beautiful day in Portland!' + }, + { + 'author': {'nickname': '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']) +@oid.loginhandler +def login(): + if g.user is not None and g.user.is_authenticated: + return redirect(url_for('index')) + form = LoginForm() + if form.validate_on_submit(): + session['remember_me'] = form.remember_me.data + return oid.try_login(form.openid.data, ask_for=['nickname', 'email']) + return render_template('login.html', + title='Sign In', + form=form, + providers=app.config['OPENID_PROVIDERS']) + + +@oid.after_login +def after_login(resp): + if resp.email is None or resp.email == "": + flash('Invalid login. Please try again.') + return redirect(url_for('login')) + user = User.query.filter_by(email=resp.email).first() + if user is None: + nickname = resp.nickname + if nickname is None or nickname == "": + nickname = resp.email.split('@')[0] + nickname = User.make_unique_nickname(nickname) + user = User(nickname=nickname, email=resp.email) + db.session.add(user) + db.session.commit() + remember_me = False + if 'remember_me' in session: + remember_me = session['remember_me'] + session.pop('remember_me', None) + login_user(user, remember=remember_me) + return redirect(request.args.get('next') or url_for('index')) + + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('login')) + + +@app.route('/user/') +@login_required +def user(nickname): + user = User.query.filter_by(nickname=nickname).first() + if user is None: + flash('User %s not found.' % nickname) + return redirect(url_for('index')) + posts = [ + {'author': user, 'body': 'Test post #1'}, + {'author': user, 'body': 'Test post #2'} + ] + return render_template('user.html', + user=user, + posts=posts) + + +@app.route('/edit', methods=['GET', 'POST']) +@login_required +def edit(): + form = EditForm(g.user.nickname) + if form.validate_on_submit(): + g.user.nickname = form.nickname.data + g.user.about_me = form.about_me.data + db.session.add(g.user) + db.session.commit() + flash('Your changes have been saved.') + return redirect(url_for('edit')) + elif request.method != "POST": + form.nickname.data = g.user.nickname + form.about_me.data = g.user.about_me + return render_template('edit.html', form=form) + diff --git a/config.py b/config.py new file mode 100644 index 0000000..a5ee9ad --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') +SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') +SQLALCHEMY_TRACK_MODIFICATIONS = True + +WTF_CSRF_ENABLED = True +SECRET_KEY = '$4$Y0NTxwTG$Vg4OKd3lNTB7vpQa5dAJcCLbDkM$' + +OPENID_PROVIDERS = [ + {'name': 'datanom', 'url': 'https://openid.datanom.net/'}, + {'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'}, + {'name': 'Yahoo', 'url': 'https://me.yahoo.com'}, + {'name': 'AOL', 'url': 'http://openid.aol.com/'}, + {'name': 'Flickr', 'url': 'http://www.flickr.com/'}, + {'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}] + +# mail server settings +MAIL_SERVER = 'localhost' +MAIL_PORT = 25 +MAIL_USERNAME = None +MAIL_PASSWORD = None + +# administrator list +ADMINS = ['you@example.com'] + diff --git a/db_create.py b/db_create.py new file mode 100755 index 0000000..f35f9bc --- /dev/null +++ b/db_create.py @@ -0,0 +1,12 @@ +#!flask/bin/python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +from app import db +import os.path +db.create_all() +if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): + api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') + api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +else: + api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, api.version(SQLALCHEMY_MIGRATE_REPO)) diff --git a/db_downgrade.py b/db_downgrade.py new file mode 100755 index 0000000..59200a9 --- /dev/null +++ b/db_downgrade.py @@ -0,0 +1,8 @@ +#!flask/bin/python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('Current database version: ' + str(v)) diff --git a/db_migrate.py b/db_migrate.py new file mode 100755 index 0000000..f5fce56 --- /dev/null +++ b/db_migrate.py @@ -0,0 +1,17 @@ +#!flask/bin/python +import imp +from migrate.versioning import api +from app import db +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +migration = SQLALCHEMY_MIGRATE_REPO + ('/versions/%03d_migration.py' % (v+1)) +tmp_module = imp.new_module('old_model') +old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +exec(old_model, tmp_module.__dict__) +script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata) +open(migration, "wt").write(script) +api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('New migration saved as ' + migration) +print('Current database version: ' + str(v)) diff --git a/db_upgrade.py b/db_upgrade.py new file mode 100755 index 0000000..d17f322 --- /dev/null +++ b/db_upgrade.py @@ -0,0 +1,7 @@ +#!flask/bin/python +from migrate.versioning import api +from config import SQLALCHEMY_DATABASE_URI +from config import SQLALCHEMY_MIGRATE_REPO +api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) +print('Current database version: ' + str(v)) diff --git a/setup.sh b/setup.sh index 54cbfac..cc63b97 100755 --- a/setup.sh +++ b/setup.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +python3 -m venv flask flask/bin/pip install flask flask/bin/pip install flask-login #flask/bin/pip install flask-openid