Merge branch 'user_handling' into 'master'

User handling



See merge request !4
This commit is contained in:
Joachim Lusiardi 2016-04-23 17:35:10 +02:00
commit 3ec2e4648d
14 changed files with 145 additions and 218 deletions

184
.gitignore vendored
View File

@ -1,181 +1,3 @@
### Python template .idea/
# Byte-compiled / optimized / DLL files **.DS_Store
__pycache__/ **.swp
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask instance folder
instance/
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# Created by .ignore support plugin (hsz.mobi)

View File

@ -7,5 +7,6 @@ RUN pip3 install -r /requirements.txt; \
ADD app /app ADD app /app
VOLUME ["/data"] VOLUME ["/data"]
VOLUME ["/app/config]
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT python3 /app/main.py ENTRYPOINT python3 /app/main.py

View File

@ -3,6 +3,14 @@
## build ## build
`docker build --tag=$(basename $PWD) .` `docker build --tag=$(basename $PWD) .`
## general configuration
Look at *app/config/email.py.example** for the configuration of the
parameters required for sending emails. Copy the file as *email.py* to
a folder that will serve as configuration directory and fill in the
information. The directory will be used as volume during container
operation.
## run in development ## run in development
Include the development version of the code as volume, so the app gets Include the development version of the code as volume, so the app gets
@ -10,7 +18,7 @@ reloaded automatically. The sqlite file will be stored in *tmp* so it
can be inspected with tools like *sqlite3*. The switch *DEBUG* enables can be inspected with tools like *sqlite3*. The switch *DEBUG* enables
debugging during development. debugging during development.
`docker run --name rollerverbrauch -ti -v `pwd`/app:/app -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch` `docker run --rm --name rollerverbrauch -ti -v `pwd`/app:/app -v `pwd`/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch`
## run in production ## run in production
`docker run --name pitstops -d -v /data/pitstops/:/data -p 80:5000 rollerverbrauch` `docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch`

View File

@ -0,0 +1,6 @@
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'your-gmail-username'
MAIL_PASSWORD = 'your-gmail-password'

View File

@ -1,15 +1,14 @@
from datetime import date from datetime import date
from datetime import datetime
from flask import Flask from flask import Flask
from flask import render_template, make_response from flask import render_template, make_response
from flask import request, redirect, g from flask import request, redirect, g
from flask import url_for from flask import url_for
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, \ from flask.ext.security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin, login_required, utils UserMixin, RoleMixin, login_required, roles_required, utils
import uuid from flask.ext.security import user_registered
import hashlib from flask.ext.mail import Mail, Message
from functools import wraps from flask_security.core import current_user
from flask_wtf import Form from flask_wtf import Form
from wtforms import DateField, IntegerField, DecimalField from wtforms import DateField, IntegerField, DecimalField
from wtforms.validators import DataRequired, ValidationError from wtforms.validators import DataRequired, ValidationError
@ -20,11 +19,14 @@ DATABASE = '/data/rollerverbrauch.db'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+DATABASE app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+DATABASE
db = SQLAlchemy(app) db = SQLAlchemy(app)
app.config['SECRET_KEY'] = 'development key'
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
app.config['SECURITY_PASSWORD_SALT'] = 'xxxxxxxxxxxxxxxxxxxxxx' app.config['SECURITY_REGISTERABLE'] = True
app.config['SECURITY_CHANGEABLE'] = True
app.config['SECURITY_RECOVERABLE'] = True
app.config.from_envvar('config')
app.config.from_object(__name__) app.config.from_object(__name__)
mail = Mail(app)
roles_users = db.Table('roles_users', roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
@ -56,7 +58,7 @@ class User(db.Model, UserMixin):
) )
def __repr__(self): def __repr__(self):
return '<User %r>' % self.username return '<User %r>' % self.email
class Pitstop(db.Model): class Pitstop(db.Model):
@ -77,26 +79,21 @@ class Pitstop(db.Model):
user_datastore = SQLAlchemyUserDatastore(db, User, Role) user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore) security = Security(app, user_datastore)
@user_registered.connect_via(app)
def user_registered_sighandler(app, user, confirm_token):
"""
Called after a user was created
"""
role = user_datastore.find_role('user')
user_datastore.add_role_to_user(user, role)
@app.before_first_request @app.before_first_request
def before_first_request(): def before_first_request():
db.create_all() db.create_all()
user_datastore.find_or_create_role(name='admin', description='Role for administrators')
user_datastore.find_or_create_role(name='admin', description='Administrator') user_datastore.find_or_create_role(name='user', description='Role for all users.')
user_datastore.find_or_create_role(name='end-user', description='End user')
encrypted_password = utils.encrypt_password('password')
if not user_datastore.get_user('someone@example.com'):
user_datastore.create_user(email='someone@example.com', password=encrypted_password)
if not user_datastore.get_user('admin@example.com'):
user_datastore.create_user(email='admin@example.com', password=encrypted_password)
# Commit any database changes; the User and Roles must exist before we can add a Role to the User
db.session.commit()
# Give one User has the "end-user" role, while the other has the "admin" role. (This will have no effect if the
# Users already have these Roles.) Again, commit any database changes.
user_datastore.add_role_to_user('someone@example.com', 'end-user')
user_datastore.add_role_to_user('admin@example.com', 'admin')
db.session.commit() db.session.commit()
@ -173,6 +170,20 @@ def get_manual():
return render_template('manual.html', data=g.data) return render_template('manual.html', data=g.data)
@app.route('/admin', methods=['GET'])
@roles_required('admin')
def get_admin_page():
g.data['users'] = User.query.all()
return render_template('admin.html', data=g.data)
@app.route('/account', methods=['GET'])
@login_required
def get_account_page():
print(current_user)
return render_template('account.html', data=g.data)
@app.route('/statistics', methods=['GET']) @app.route('/statistics', methods=['GET'])
@login_required @login_required
def get_statistics(): def get_statistics():

View File

@ -0,0 +1,7 @@
{% extends "layout.html" %}
{% block body %}
<h1>Account management for {{current_user.email}}</h1>
<a href='{{ url_for('security.change_password') }}'>Change password</a>
{% endblock %}

12
app/templates/admin.html Normal file
View File

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block body %}
<h1>Admin</h1>
We have {{ data.users|length }} users so far:
<ul>
{% for user in data.users %}
<li>{{user.email}}</li>
{% endfor %}
</ul>
<a href='{{ url_for('security.login', _external=True) }}'>Login</a>
{% endblock %}

View File

@ -1,9 +1,15 @@
{% macro navigation() -%} {% macro navigation() -%}
<li><a href='{{ url_for('create_pit_stop_form') }}'>Create Pitstop</a></li>
<li><a href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a href='{{ url_for('get_manual') }}'>Manual</a></li>
{% if current_user.email %} {% if current_user.email %}
<li><a href='{{ url_for('create_pit_stop_form') }}'>Create Pitstop</a></li>
<li><a href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a href='{{ url_for('get_account_page') }}'>Account</a></li>
{% if current_user.has_role('admin') %}
<li><a href='{{ url_for('get_admin_page') }}'>Admin</a></li>
{% endif %}
<li><a href='{{ url_for('security.logout') }}'>Logout</a></li> <li><a href='{{ url_for('security.logout') }}'>Logout</a></li>
{% else %}
<li><a href='{{ url_for('security.login') }}'>Login</a></li>
<li><a href='{{ url_for('security.register') }}'>Register</a></li>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}

View File

@ -0,0 +1,13 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Change password</h1>
<form class='form-horizontal' action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field(change_password_form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Send password reset instructions</h1>
<form class='form-horizontal' action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field(forgot_password_form.submit) }}
</form>
{% endblock %}

View File

@ -2,13 +2,16 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %} {% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %} {% block body %}
<h1>Login</h1>
<form class='form-horizontal' action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> <form class='form-horizontal' action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }} {{ login_user_form.hidden_tag() }}
{{ render_field_with_errors(login_user_form.email) }} {{ render_field_with_errors(login_user_form.email) }}
{{ render_field_with_errors(login_user_form.password) }} {{ render_field_with_errors(login_user_form.password) }}
{{ render_field_with_errors(login_user_form.remember) }} {{ render_field_with_errors(login_user_form.remember) }}
{{ render_field(login_user_form.next) }} {{ render_field(login_user_form.next) }}
{{ render_field(login_user_form.submit) }} {{ render_field(login_user_form.submit) }}
{% if security.recoverable %}
<a href="{{ url_for_security('forgot_password') }}">Forgot password</a>
{% endif %}
</form> </form>
{% include "security/_menu.html" %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Register User</h1>
<form class='form-horizontal' action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_field_with_errors(register_user_form.email) }}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field(register_user_form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Reset password</h1>
<form class='form-horizontal' action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
{{ render_field_with_errors(reset_password_form.password) }}
{{ render_field_with_errors(reset_password_form.password_confirm) }}
{{ render_field(reset_password_form.submit) }}
</form>
{% endblock %}