]> git.datanom.net - flask-test.git/commitdiff
First half
authorMichael Rasmussen <mir@datanom.net>
Wed, 29 Nov 2017 03:12:38 +0000 (04:12 +0100)
committerMichael Rasmussen <mir@datanom.net>
Wed, 29 Nov 2017 03:12:38 +0000 (04:12 +0100)
Signed-off-by: Michael Rasmussen <mir@datanom.net>
19 files changed:
.gitignore
app/__init__.py
app/forms.py [new file with mode: 0644]
app/models.py [new file with mode: 0644]
app/templates/404.html [new file with mode: 0644]
app/templates/500.html [new file with mode: 0644]
app/templates/base.html [new file with mode: 0644]
app/templates/edit.html [new file with mode: 0644]
app/templates/index.html [new file with mode: 0644]
app/templates/login.html [new file with mode: 0644]
app/templates/post.html [new file with mode: 0644]
app/templates/user.html [new file with mode: 0644]
app/views.py
config.py [new file with mode: 0644]
db_create.py [new file with mode: 0755]
db_downgrade.py [new file with mode: 0755]
db_migrate.py [new file with mode: 0755]
db_upgrade.py [new file with mode: 0755]
setup.sh

index e9bd007bd294332e5806353a01431d3e03b33f52..013141fba69dabf515b88180964fc53d4ed6c341 100644 (file)
@@ -1,2 +1,6 @@
 flask/
+db_repository/
+app.db
+tmp/
+__pycache__/
 app/__pycache__/
index 3ab3eef88286e80be773e4031144d0d4f7cf8fdc..8cfa1fbe8f2ac34fe5bfc0face26471995eca77a 100644 (file)
@@ -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 (file)
index 0000000..8c0d16c
--- /dev/null
@@ -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 (file)
index 0000000..6013939
--- /dev/null
@@ -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 '<User %r>' % (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 '<Post %r>' % (self.body)
+
diff --git a/app/templates/404.html b/app/templates/404.html
new file mode 100644 (file)
index 0000000..1a02eee
--- /dev/null
@@ -0,0 +1,7 @@
+<!-- extend base layout -->
+{% extends "base.html" %}
+
+{% block content %}
+  <h1>File Not Found</h1>
+  <p><a href="{{ url_for('index') }}">Back</a></p>
+{% endblock %}
diff --git a/app/templates/500.html b/app/templates/500.html
new file mode 100644 (file)
index 0000000..bda4fd0
--- /dev/null
@@ -0,0 +1,8 @@
+<!-- extend base layout -->
+{% extends "base.html" %}
+
+{% block content %}
+  <h1>An unexpected error has occurred</h1>
+  <p>The administrator has been notified. Sorry for the inconvenience!</p>
+  <p><a href="{{ url_for('index') }}">Back</a></p>
+{% endblock %}
diff --git a/app/templates/base.html b/app/templates/base.html
new file mode 100644 (file)
index 0000000..5a4dcdc
--- /dev/null
@@ -0,0 +1,29 @@
+<html>
+  <head>
+    {% if title %}
+    <title>{{ title }} - Web Photo Portfolio</title>
+    {% else %}
+    <title>Welcome to Web Photo Portfolio</title>
+    {% endif %}
+  </head>
+  <body>
+    <div>WPP: <a href="/index">Home</a>
+    {% if g.user.is_authenticated %}
+    | <a href="{{ url_for('user', nickname=g.user.nickname) }}">Your Profile</a>
+    | <a href="{{ url_for('logout') }}">Logout</a>
+    {% endif %}
+    </div>
+    <hr />
+    {% with messages = get_flashed_messages() %}
+      {% if messages %}
+        <ul>
+        {% for message in messages %}
+            <li>{{ message }} </li>
+        {% endfor %}
+        </ul>
+      {% endif %}
+    {% endwith %}
+    {% block content %}{% endblock %}
+  </body>
+</html>
+
diff --git a/app/templates/edit.html b/app/templates/edit.html
new file mode 100644 (file)
index 0000000..5e3c460
--- /dev/null
@@ -0,0 +1,29 @@
+<!-- extend base layout -->
+{% extends "base.html" %}
+
+{% block content %}
+  <h1>Edit Your Profile</h1>
+  <form action="" method="post" name="edit">
+      {{form.hidden_tag()}}
+      <table>
+          <tr>
+              <td>Your nickname:</td>
+              <td>
+               {{ form.nickname(size=24) }}
+               {% for error in form.errors.nickname %}
+               <br><span style="color: red;">[{{ error }}]</span>
+               {% endfor %}
+             </td>
+          </tr>
+          <tr>
+              <td>About yourself:</td>
+              <td>{{ form.about_me(cols=32, rows=4) }}</td>
+          </tr>
+          <tr>
+              <td></td>
+              <td><input type="submit" value="Save Changes"></td>
+          </tr>
+      </table>
+  </form>
+{% endblock %}
+
diff --git a/app/templates/index.html b/app/templates/index.html
new file mode 100644 (file)
index 0000000..80a6eb3
--- /dev/null
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% block content %}
+    <h1>Hi, {{ user.nickname }}!</h1>
+    {% for post in posts %}
+    <div><p>{{ post.author.nickname }} says: <b>{{ post.body }}</b></p></div>
+    {% endfor %}
+{% endblock %}
+
diff --git a/app/templates/login.html b/app/templates/login.html
new file mode 100644 (file)
index 0000000..f28940b
--- /dev/null
@@ -0,0 +1,36 @@
+<!-- extend from base layout -->
+{% extends "base.html" %}
+
+{% block content %}
+<script type="text/javascript">
+function set_openid(openid, pr)
+{
+    u = openid.search('<username>')
+    if (u != -1) {
+        // openid requires username
+        user = prompt('Enter your ' + pr + ' username:')
+        openid = openid.substr(0, u) + user
+    }
+    form = document.forms['login'];
+    form.elements['openid'].value = openid
+}
+</script>
+  <h1>Sign In</h1>
+  <h4>{{ error }}</h4>
+  <form action="" method="post" name="login">
+      {{ form.hidden_tag() }}
+      <p>
+          Please enter your OpenID:<br>
+          {{ form.openid(size=80) }}<br>
+          {% for error in form.openid.errors %}
+            <span style="color: red;">[{{ error }}]</span>
+          {% endfor %}<br>
+         |{% for pr in providers %}
+          <a href="javascript:set_openid('{{ pr.url }}', '{{ pr.name }}');">{{ pr.name }}</a> |
+        {% endfor %}
+      </p>
+      <p>{{ form.remember_me }} Remember Me</p>
+      <p><input type="hidden" name="next" value="{{ next }}"></p>
+      <p><input type="submit" value="Sign In"></p>
+  </form>
+{% endblock %}
diff --git a/app/templates/post.html b/app/templates/post.html
new file mode 100644 (file)
index 0000000..d55ba60
--- /dev/null
@@ -0,0 +1,5 @@
+<table>
+    <tr valign="top">
+        <td><img src="{{ post.author.avatar(50) }}"></td><td><i>{{ post.author.nickname }} says:</i><br>{{ post.body }}</td>
+    </tr>
+</table>
diff --git a/app/templates/user.html b/app/templates/user.html
new file mode 100644 (file)
index 0000000..7e76e61
--- /dev/null
@@ -0,0 +1,26 @@
+<!-- extend base layout -->
+{% extends "base.html" %}
+
+{% block content %}
+  <table>
+      <tr valign="top">
+          <td><img src="{{ user.avatar(128) }}"></td>
+          <td>
+           <h1>User: {{ user.nickname }}</h1>
+               {% if user.about_me %}
+                   <p>{{ user.about_me }}</p>
+               {% endif %}
+               {% if user.last_seen %}
+                   <p><i>Last seen on: {{ user.last_seen }}</i></p>
+               {% endif %}
+               {% if user.id == g.user.id %}
+                   <p><a href="{{ url_for('edit') }}">Edit</a></p>
+               {% endif %}
+         </td>
+      </tr>
+  </table>
+  <hr>
+  {% for post in posts %}
+      {% include 'post.html' %}
+  {% endfor %}
+{% endblock %}
index d467b1595f293974b42ff882d32728768160705d..e00bb0d81f5acba91f094519c1499ccfd7dbb33c 100644 (file)
@@ -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/<nickname>')
+@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 (file)
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/<username>'},
+    {'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'},
+    {'name': 'Yahoo', 'url': 'https://me.yahoo.com'},
+    {'name': 'AOL', 'url': 'http://openid.aol.com/<username>'},
+    {'name': 'Flickr', 'url': 'http://www.flickr.com/<username>'},
+    {'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 (executable)
index 0000000..f35f9bc
--- /dev/null
@@ -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 (executable)
index 0000000..59200a9
--- /dev/null
@@ -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 (executable)
index 0000000..f5fce56
--- /dev/null
@@ -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 (executable)
index 0000000..d17f322
--- /dev/null
@@ -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))
index 54cbfac7c0200a6f03b76bdaf23446ef7a5595bb..cc63b97890a2c549b793821564e7f7dbd43c154d 100755 (executable)
--- 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
This page took 0.063406 seconds and 5 git commands to generate.