aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/__init__.py3
-rw-r--r--app/forms.py28
-rw-r--r--app/models.py17
-rw-r--r--app/routes.py53
-rw-r--r--app/templates/base.html4
-rw-r--r--app/templates/index.html2
-rw-r--r--app/templates/login.html1
-rw-r--r--app/templates/register.html37
-rw-r--r--migrations/versions/f3622e20dd86_initialize.py67
-rw-r--r--poetry.lock14
-rw-r--r--pyproject.toml1
11 files changed, 213 insertions, 14 deletions
diff --git a/app/__init__.py b/app/__init__.py
index 996b683..ae7e240 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -3,11 +3,14 @@ __version__ = "0.1.0"
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
+from flask_login import LoginManager
from .config import Config
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
+login = LoginManager(app)
+login.login_view = 'login'
from app import routes, models
diff --git a/app/forms.py b/app/forms.py
index 919db70..75de178 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -1,6 +1,11 @@
+
from flask_wtf import FlaskForm
+
from wtforms import StringField, PasswordField, BooleanField, SubmitField
-from wtforms.validators import DataRequired
+from wtforms.validators import DataRequired, ValidationError, Email, EqualTo
+
+from app.models import User
+
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
@@ -9,4 +14,25 @@ class LoginForm(FlaskForm):
submit = SubmitField("Sign In")
+class RegistrationForm(FlaskForm):
+ username = StringField("Username", validators=[DataRequired()])
+ email = StringField("Email", validators=[DataRequired(), Email()])
+ password = PasswordField("Password", validators=[DataRequired()])
+ password2 = PasswordField(
+ "Repeat Password", validators=[DataRequired(), EqualTo("password")]
+ )
+ submit = SubmitField("Register")
+
+ def validate_username(self, username):
+ user = User.query.filter_by(username=username.data).first()
+ if user is not None:
+ raise ValidationError(
+ "Username already in use, please use a different one."
+ )
+ def validate_email(self, email):
+ user = User.query.filter_by(email=email.data).first()
+ if user is not None:
+ raise ValidationError(
+ "Email address already in use, please use a different one."
+ )
diff --git a/app/models.py b/app/models.py
index 9d11ffb..b607748 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,11 +1,15 @@
import datetime
+from flask_login import UserMixin
+
+from werkzeug.security import generate_password_hash, check_password_hash
from app import db
+from app import login
-class User(db.Model):
+class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(), unique=True, nullable=False)
@@ -17,6 +21,12 @@ class User(db.Model):
def __repr__(self):
return "<User %r>" % self.username
+ def set_password(self, password):
+ self.password_hash = generate_password_hash(password)
+
+ def check_password(self, password):
+ return check_password_hash(self.password_hash, password)
+
class Dataset(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -57,3 +67,8 @@ class Annotation(db.Model):
def __repr__(self):
return "<Annotation %r>" % self.id
+
+
+@login.user_loader
+def load_user(_id):
+ return User.query.get(int(_id))
diff --git a/app/routes.py b/app/routes.py
index 2f9d9b8..44600db 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -1,24 +1,57 @@
-from flask import render_template, flash, redirect, url_for
+
+from flask import render_template, flash, redirect, url_for, request
+from flask_login import current_user, login_user, logout_user, login_required
+
+from werkzeug.urls import url_parse
+
from app import app
-from app.forms import LoginForm
+from app import db
+from app.forms import LoginForm, RegistrationForm
+from app.models import User
@app.route("/")
@app.route("/index")
+@login_required
def index():
- user = {"username": "Gertjan"}
- return render_template("index.html", title="Home", user=user)
+ return render_template("index.html", title="Home")
@app.route("/login", methods=("GET", "POST"))
def login():
+ if current_user.is_authenticated:
+ return redirect(url_for("index"))
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"))
+ user = User.query.filter_by(username=form.username.data).first()
+ if user is None or not user.check_password(form.password.data):
+ flash("Invalid username or password", category="error")
+ return redirect(url_for("login"))
+ login_user(user, remember=form.remember_me.data)
+ next_page = request.args.get("next")
+ if not next_page or url_parse(next_page).netloc != "":
+ next_page = url_for("index")
+ return redirect(next_page)
return render_template("login.html", title="Sign In", form=form)
+
+
+@app.route("/logout")
+def logout():
+ logout_user()
+ return redirect(url_for("index"))
+
+
+@app.route("/register", methods=("GET", "POST"))
+def register():
+ if current_user.is_authenticated:
+ return redirect(url_for("index"))
+ form = RegistrationForm()
+ if form.validate_on_submit():
+ user = User(username=form.username.data, email=form.email.data)
+ user.set_password(form.password.data)
+ db.session.add(user)
+ db.session.commit()
+ flash("Thank you, you are now a registered user!")
+ return redirect(url_for("login"))
+ return render_template("register.html", title="Register", form=form)
diff --git a/app/templates/base.html b/app/templates/base.html
index c40482d..3070587 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -9,7 +9,11 @@
<body>
<div>Annotate Change:
<a href="{{ url_for('index') }}">Home</a>
+ {% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
+ {% else %}
+ <a href="{{ url_for('logout') }}">Logout</a>
+ {% endif %}
</div>
<hr>
{% with messages = get_flashed_messages() %}
diff --git a/app/templates/index.html b/app/templates/index.html
index f111cd6..3cf36ee 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block content %}
-<h1>Hi, {{ user.username }}!</h1>
+<h1>Hi, {{ current_user.username }}!</h1>
{% endblock %}
diff --git a/app/templates/login.html b/app/templates/login.html
index 21e0161..77410a1 100644
--- a/app/templates/login.html
+++ b/app/templates/login.html
@@ -21,4 +21,5 @@
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
+ <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>
{% endblock %}
diff --git a/app/templates/register.html b/app/templates/register.html
new file mode 100644
index 0000000..487b860
--- /dev/null
+++ b/app/templates/register.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+
+{% block content %}
+ <h1>Register</h1>
+ <form action="" method="post">
+ {{ form.hidden_tag() }}
+ <p>
+ {{ form.username.label }}<br>
+ {{ form.username(size=32) }}<br>
+ {% for error in form.username.errors %}
+ <span style="color: red;">[{{ error }}]</span>
+ {% endfor %}
+ </p>
+ <p>
+ {{ form.email.label }}<br>
+ {{ form.email(size=64) }}<br>
+ {% for error in form.email.errors %}
+ <span style="color: red;">[{{ error }}]</span>
+ {% endfor %}
+ </p>
+ <p>
+ {{ form.password.label }}<br>
+ {{ form.password(size=32) }}<br>
+ {% for error in form.password.errors %}
+ <span style="color: red;">[{{ error }}]</span>
+ {% endfor %}
+ </p>
+ <p>
+ {{ form.password2.label }}<br>
+ {{ form.password2(size=32) }}<br>
+ {% for error in form.password2.errors %}
+ <span style="color: red;">[{{ error }}]</span>
+ {% endfor %}
+ </p>
+ <p>{{ form.submit() }}</p>
+ </form>
+{% endblock %}
diff --git a/migrations/versions/f3622e20dd86_initialize.py b/migrations/versions/f3622e20dd86_initialize.py
new file mode 100644
index 0000000..545ca5b
--- /dev/null
+++ b/migrations/versions/f3622e20dd86_initialize.py
@@ -0,0 +1,67 @@
+"""initialize
+
+Revision ID: f3622e20dd86
+Revises:
+Create Date: 2019-03-18 14:11:22.386217
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'f3622e20dd86'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('dataset',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(), nullable=False),
+ sa.Column('created', sa.DateTime(), nullable=False),
+ sa.Column('md5sum', sa.String(length=32), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('md5sum'),
+ sa.UniqueConstraint('name')
+ )
+ op.create_table('user',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=80), nullable=False),
+ sa.Column('email', sa.String(), nullable=False),
+ sa.Column('password_hash', sa.String(length=128), nullable=False),
+ sa.Column('last_active', sa.DateTime(), nullable=False),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email'),
+ sa.UniqueConstraint('username')
+ )
+ op.create_table('task',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('done', sa.Boolean(), nullable=False),
+ sa.Column('annotated_on', sa.DateTime(), nullable=True),
+ sa.Column('annotator_id', sa.Integer(), nullable=True),
+ sa.Column('dataset_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['annotator_id'], ['user.id'], ),
+ sa.ForeignKeyConstraint(['dataset_id'], ['dataset.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('annotation',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('time_start', sa.Integer(), nullable=True),
+ sa.Column('time_end', sa.Integer(), nullable=True),
+ sa.Column('task_id', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['task_id'], ['task.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('annotation')
+ op.drop_table('task')
+ op.drop_table('user')
+ op.drop_table('dataset')
+ # ### end Alembic commands ###
diff --git a/poetry.lock b/poetry.lock
index d2492e5..f617752 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -61,6 +61,17 @@ itsdangerous = ">=0.24"
[[package]]
category = "main"
+description = "User session management for Flask"
+name = "flask-login"
+optional = false
+python-versions = "*"
+version = "0.4.1"
+
+[package.dependencies]
+Flask = "*"
+
+[[package]]
+category = "main"
description = "SQLAlchemy database migrations for Flask applications using Alembic"
name = "flask-migrate"
optional = false
@@ -228,7 +239,7 @@ python-versions = "*"
version = "2.2.1"
[metadata]
-content-hash = "71d5d24e1ceaef122c936d2bf4afdf2eeecd408f93268332605e045c7a054472"
+content-hash = "e6689d7bbc818777fee652ee8c8b6d54bf9ceb5bbce6c67adf74b39add3ecd48"
python-versions = "^3.7"
[metadata.hashes]
@@ -238,6 +249,7 @@ attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
flask = ["2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", "a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"]
+flask-login = ["c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec"]
flask-migrate = ["a361578cb829681f860e4de5ed2c48886264512f0c16144e404c36ddc95ab49c", "c24d105c5d6cc670de20f8cbfb909e04f4e04b8784d0df070005944de1f21549"]
flask-sqlalchemy = ["3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", "5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"]
flask-wtf = ["5d14d55cfd35f613d99ee7cba0fc3fbbe63ba02f544d349158c14ca15561cc36", "d9a9e366b32dcbb98ef17228e76be15702cd2600675668bca23f63a7947fd5ac"]
diff --git a/pyproject.toml b/pyproject.toml
index 3154708..2372f40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ flask = "^1.0"
flask-wtf = "^0.14.2"
flask-sqlalchemy = "^2.3"
flask-migrate = "^2.4"
+flask-login = "^0.4.1"
[tool.poetry.dev-dependencies]
pytest = "^3.0"