diff --git a/.gitignore b/.gitignore index 28ebf13..45c35c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ **.DS_Store **.swp +**.pyc diff --git a/app/main.py b/app/main.py index 147e7ab..77519c8 100644 --- a/app/main.py +++ b/app/main.py @@ -1,236 +1,5 @@ -from datetime import date -from flask import Flask -from flask import render_template, make_response -from flask import request, redirect, g -from flask import url_for -from flask_sqlalchemy import SQLAlchemy -from flask.ext.security import Security, SQLAlchemyUserDatastore, \ - UserMixin, RoleMixin, login_required, roles_required, utils -from flask.ext.security import user_registered -from flask.ext.mail import Mail, Message -from flask_security.core import current_user -from flask_wtf import Form -from wtforms import DateField, IntegerField, DecimalField -from wtforms.validators import DataRequired, ValidationError import os - -app = Flask(__name__) -DATABASE = '/data/rollerverbrauch.db' -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+DATABASE -db = SQLAlchemy(app) - -app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' -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__) - -mail = Mail(app) - -roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), - db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) - - -class Role(db.Model, RoleMixin): - id = db.Column(db.Integer(), primary_key=True) - name = db.Column(db.String(80), unique=True) - description = db.Column(db.String(255)) - - def __str__(self): - return self.name - - def __hash__(self): - return hash(self.name) - - -class User(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(255), unique=True) - password = db.Column(db.String(255)) - active = db.Column(db.Boolean()) - confirmed_at = db.Column(db.DateTime()) - roles = db.relationship( - 'Role', - secondary=roles_users, - backref=db.backref('users', lazy='dynamic') - ) - - def __repr__(self): - return '' % self.email - - -class Pitstop(db.Model): - id = db.Column(db.Integer, primary_key=True) - date = db.Column(db.Date) - odometer = db.Column(db.Integer) - litres = db.Column(db.Numeric(5,2)) - - def __init__(self, odometer, litres, date): - self.odometer = odometer - self.litres = litres - self.date = date - - def __repr__(self): - return '' % (self.odometer, self.litres) - - -user_datastore = SQLAlchemyUserDatastore(db, User, Role) -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 -def before_first_request(): - db.create_all() - user_datastore.find_or_create_role(name='admin', description='Role for administrators') - user_datastore.find_or_create_role(name='user', description='Role for all users.') - db.session.commit() - - -@app.before_request -def before_request(): - g.data = {} - - -@app.route('/') -@login_required -def index(): - return redirect(url_for('get_pit_stops')) - - -def date_check(form, field): - if field.data < form.pitstop.date: - raise ValidationError('The new date must after %s' % form.pitstop.date) - - -def odometer_check(form, field): - if field.data <= form.pitstop.odometer: - raise ValidationError('The new odometer value must be higher than %i km' % form.pitstop.odometer) - - -def litres_check(form, field): - if field.data is not None and field.data <= 0: - raise ValidationError('You must fuel at least 0.1 l') - - -class CreatePitstopForm(Form): - date = DateField('Date of Pitstop', validators=[date_check]) - odometer = IntegerField('Odometer (km)', validators=[odometer_check]) - litres = DecimalField('Litres (l)', places=1, validators=[litres_check]) - pitstop = None - - def set_pitstop(self, pitstop): - self.pitstop = pitstop - - -@app.route('/pitstops/createForm', methods=['GET', 'POST']) -@login_required -def create_pit_stop_form(): - last_pitstop = Pitstop.query.order_by(Pitstop.id.desc()).first() - if last_pitstop is None: - last_pitstop = Pitstop(0, 0, date.today()) - - form = CreatePitstopForm() - form.set_pitstop(last_pitstop) - if form.validate_on_submit(): - new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data) - db.session.add(new_stop) - db.session.commit() - return redirect(url_for('get_pit_stops')) - - # dynamically set values - form.odometer.default = last_pitstop.odometer - form.litres.default = last_pitstop.litres - form.date.default = date.today() - form.process() - return render_template('newPitStopForm.html', form=form) - - -@app.route('/pitstops', methods=['GET']) -@login_required -def get_pit_stops(): - data = prepare_pit_stops(Pitstop.query.all()) - g.data['pitstops'] = data - return render_template('pitstops.html', data=g.data) - - -@app.route('/manual', methods=['GET']) -@login_required -def get_manual(): - 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']) -@login_required -def get_statistics(): - pitstops = Pitstop.query.all() - count = len(pitstops) - distance = 0 - sum_litres = 0 - average_distance = 0 - average_litres_fuelled = 0 - average_litres_used = 0 - - if count > 0: - sum_litres = 0 - for pitstop in pitstops: - sum_litres += pitstop.litres - average_litres_fuelled = sum_litres/count - if count > 1: - distance = pitstops[-1].odometer - pitstops[0].odometer - average_distance = distance/(count - 1) - average_litres_used = 100 * (sum_litres-pitstops[0].litres)/distance - g.data['distance'] = distance - g.data['count'] = count - g.data['litres'] = sum_litres - g.data['averageDistance'] = average_distance - g.data['averageListresFuelled'] = average_litres_fuelled - g.data['averageListresUsed'] = average_litres_used - return render_template('statistics.html', data=g.data) - - -def prepare_pit_stops(pss): - pitstops = [] - for pitstop_index in range(0, len(pss)): - p = dict() - p['odometer'] = pss[pitstop_index].odometer - p['litres'] = pss[pitstop_index].litres - p['date'] = pss[pitstop_index].date - pitstops.append(p) - for pitstop_index in range(1, len(pitstops)): - last = pitstops[pitstop_index - 1] - curr = pitstops[pitstop_index] - curr['distance'] = curr['odometer'] - last['odometer'] - curr['average'] = 100 * curr['litres']/curr['distance'] - last_date = last['date'] - curr_date = curr['date'] - curr['days'] = (curr_date - last_date).days - pitstops.reverse() - return pitstops +from rollerverbrauch import app if __name__ == '__main__': DEBUG = 'DEBUG' in os.environ and os.environ['DEBUG'] != 'False' diff --git a/app/rollerverbrauch/__init__.py b/app/rollerverbrauch/__init__.py new file mode 100644 index 0000000..544fbe0 --- /dev/null +++ b/app/rollerverbrauch/__init__.py @@ -0,0 +1,166 @@ +from datetime import date +from flask import Flask +from flask import redirect, g +from flask import render_template +from flask import url_for +from flask.ext.mail import Mail +from flask.ext.security import Security, SQLAlchemyUserDatastore, \ + UserMixin, RoleMixin, login_required, roles_required +from flask.ext.security import user_registered +from flask_security.core import current_user +from flask_sqlalchemy import SQLAlchemy + + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////data/rollerverbrauch.db' +app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' +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__) + +db = SQLAlchemy(app) +mail = Mail(app) + + +from rollerverbrauch.forms import \ + CreatePitstopForm + +from rollerverbrauch.entities import \ + User, \ + Role, \ + Pitstop + + +user_datastore = SQLAlchemyUserDatastore(db, User, Role) +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 +def before_first_request(): + db.create_all() + user_datastore.find_or_create_role(name='admin', description='Role for administrators') + user_datastore.find_or_create_role(name='user', description='Role for all users.') + db.session.commit() + + +@app.before_request +def before_request(): + g.data = {} + + +@app.route('/') +@login_required +def index(): + return redirect(url_for('get_pit_stops')) + + +@app.route('/pitstops/createForm', methods=['GET', 'POST']) +@login_required +def create_pit_stop_form(): + last_pitstop = Pitstop.query.order_by(Pitstop.id.desc()).first() + if last_pitstop is None: + last_pitstop = Pitstop(0, 0, date.today()) + + form = CreatePitstopForm() + form.set_pitstop(last_pitstop) + if form.validate_on_submit(): + new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data) + db.session.add(new_stop) + db.session.commit() + return redirect(url_for('get_pit_stops')) + + # dynamically set values + form.odometer.default = last_pitstop.odometer + form.litres.default = last_pitstop.litres + form.date.default = date.today() + form.process() + return render_template('newPitStopForm.html', form=form) + + +@app.route('/pitstops', methods=['GET']) +@login_required +def get_pit_stops(): + data = prepare_pit_stops(Pitstop.query.all()) + g.data['pitstops'] = data + return render_template('pitstops.html', data=g.data) + + +@app.route('/manual', methods=['GET']) +@login_required +def get_manual(): + 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']) +@login_required +def get_statistics(): + pitstops = Pitstop.query.all() + count = len(pitstops) + distance = 0 + sum_litres = 0 + average_distance = 0 + average_litres_fuelled = 0 + average_litres_used = 0 + + if count > 0: + sum_litres = 0 + for pitstop in pitstops: + sum_litres += pitstop.litres + average_litres_fuelled = sum_litres/count + if count > 1: + distance = pitstops[-1].odometer - pitstops[0].odometer + average_distance = distance/(count - 1) + average_litres_used = 100 * (sum_litres-pitstops[0].litres)/distance + g.data['distance'] = distance + g.data['count'] = count + g.data['litres'] = sum_litres + g.data['averageDistance'] = average_distance + g.data['averageListresFuelled'] = average_litres_fuelled + g.data['averageListresUsed'] = average_litres_used + return render_template('statistics.html', data=g.data) + + +def prepare_pit_stops(pss): + pitstops = [] + for pitstop_index in range(0, len(pss)): + p = dict() + p['odometer'] = pss[pitstop_index].odometer + p['litres'] = pss[pitstop_index].litres + p['date'] = pss[pitstop_index].date + pitstops.append(p) + for pitstop_index in range(1, len(pitstops)): + last = pitstops[pitstop_index - 1] + curr = pitstops[pitstop_index] + curr['distance'] = curr['odometer'] - last['odometer'] + curr['average'] = 100 * curr['litres']/curr['distance'] + last_date = last['date'] + curr_date = curr['date'] + curr['days'] = (curr_date - last_date).days + pitstops.reverse() + return pitstops + diff --git a/app/config/config.py.example b/app/rollerverbrauch/config/config.py.example similarity index 100% rename from app/config/config.py.example rename to app/rollerverbrauch/config/config.py.example diff --git a/app/rollerverbrauch/entities.py b/app/rollerverbrauch/entities.py new file mode 100644 index 0000000..f1dba99 --- /dev/null +++ b/app/rollerverbrauch/entities.py @@ -0,0 +1,50 @@ +from rollerverbrauch import db +from flask.ext.security import UserMixin, RoleMixin + +roles_users = db.Table('roles_users', + db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), + db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) + + +class Role(db.Model, RoleMixin): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(80), unique=True) + description = db.Column(db.String(255)) + + def __str__(self): + return self.name + + def __hash__(self): + return hash(self.name) + + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), unique=True) + password = db.Column(db.String(255)) + active = db.Column(db.Boolean()) + confirmed_at = db.Column(db.DateTime()) + roles = db.relationship( + 'Role', + secondary=roles_users, + backref=db.backref('users', lazy='dynamic') + ) + + def __repr__(self): + return '' % self.email + + +class Pitstop(db.Model): + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.Date) + odometer = db.Column(db.Integer) + litres = db.Column(db.Numeric(5,2)) + + def __init__(self, odometer, litres, date): + self.odometer = odometer + self.litres = litres + self.date = date + + def __repr__(self): + return '' % (self.odometer, self.litres) + diff --git a/app/rollerverbrauch/forms.py b/app/rollerverbrauch/forms.py new file mode 100644 index 0000000..1a317be --- /dev/null +++ b/app/rollerverbrauch/forms.py @@ -0,0 +1,30 @@ +from flask_wtf import Form +from wtforms import DateField, IntegerField, DecimalField +from wtforms.validators import ValidationError + + +def date_check(form, field): + if field.data < form.pitstop.date: + raise ValidationError('The new date must after %s' % form.pitstop.date) + + +def odometer_check(form, field): + if field.data <= form.pitstop.odometer: + raise ValidationError('The new odometer value must be higher than %i km' % form.pitstop.odometer) + + +def litres_check(form, field): + if field.data is not None and field.data <= 0: + raise ValidationError('You must fuel at least 0.1 l') + + +class CreatePitstopForm(Form): + date = DateField('Date of Pitstop', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + litres = DecimalField('Litres (l)', places=1, validators=[litres_check]) + pitstop = None + + def set_pitstop(self, pitstop): + self.pitstop = pitstop + + diff --git a/app/static/android-icon-192x192.png b/app/rollerverbrauch/static/android-icon-192x192.png similarity index 100% rename from app/static/android-icon-192x192.png rename to app/rollerverbrauch/static/android-icon-192x192.png diff --git a/app/static/apple-touch-icon-114.png b/app/rollerverbrauch/static/apple-touch-icon-114.png similarity index 100% rename from app/static/apple-touch-icon-114.png rename to app/rollerverbrauch/static/apple-touch-icon-114.png diff --git a/app/static/apple-touch-icon-120.png b/app/rollerverbrauch/static/apple-touch-icon-120.png similarity index 100% rename from app/static/apple-touch-icon-120.png rename to app/rollerverbrauch/static/apple-touch-icon-120.png diff --git a/app/static/apple-touch-icon-144.png b/app/rollerverbrauch/static/apple-touch-icon-144.png similarity index 100% rename from app/static/apple-touch-icon-144.png rename to app/rollerverbrauch/static/apple-touch-icon-144.png diff --git a/app/static/apple-touch-icon-152.png b/app/rollerverbrauch/static/apple-touch-icon-152.png similarity index 100% rename from app/static/apple-touch-icon-152.png rename to app/rollerverbrauch/static/apple-touch-icon-152.png diff --git a/app/static/apple-touch-icon-180.png b/app/rollerverbrauch/static/apple-touch-icon-180.png similarity index 100% rename from app/static/apple-touch-icon-180.png rename to app/rollerverbrauch/static/apple-touch-icon-180.png diff --git a/app/static/apple-touch-icon-57.png b/app/rollerverbrauch/static/apple-touch-icon-57.png similarity index 100% rename from app/static/apple-touch-icon-57.png rename to app/rollerverbrauch/static/apple-touch-icon-57.png diff --git a/app/static/apple-touch-icon-60.png b/app/rollerverbrauch/static/apple-touch-icon-60.png similarity index 100% rename from app/static/apple-touch-icon-60.png rename to app/rollerverbrauch/static/apple-touch-icon-60.png diff --git a/app/static/apple-touch-icon-72.png b/app/rollerverbrauch/static/apple-touch-icon-72.png similarity index 100% rename from app/static/apple-touch-icon-72.png rename to app/rollerverbrauch/static/apple-touch-icon-72.png diff --git a/app/static/apple-touch-icon-76.png b/app/rollerverbrauch/static/apple-touch-icon-76.png similarity index 100% rename from app/static/apple-touch-icon-76.png rename to app/rollerverbrauch/static/apple-touch-icon-76.png diff --git a/app/static/favicon-16.png b/app/rollerverbrauch/static/favicon-16.png similarity index 100% rename from app/static/favicon-16.png rename to app/rollerverbrauch/static/favicon-16.png diff --git a/app/static/favicon-32.png b/app/rollerverbrauch/static/favicon-32.png similarity index 100% rename from app/static/favicon-32.png rename to app/rollerverbrauch/static/favicon-32.png diff --git a/app/static/favicon-96.png b/app/rollerverbrauch/static/favicon-96.png similarity index 100% rename from app/static/favicon-96.png rename to app/rollerverbrauch/static/favicon-96.png diff --git a/app/static/jquery-1.11.2.min.js b/app/rollerverbrauch/static/jquery-1.11.2.min.js similarity index 100% rename from app/static/jquery-1.11.2.min.js rename to app/rollerverbrauch/static/jquery-1.11.2.min.js diff --git a/app/static/main.css b/app/rollerverbrauch/static/main.css similarity index 100% rename from app/static/main.css rename to app/rollerverbrauch/static/main.css diff --git a/app/static/main.js b/app/rollerverbrauch/static/main.js similarity index 100% rename from app/static/main.js rename to app/rollerverbrauch/static/main.js diff --git a/app/static/modernizr-2.8.3-respond-1.4.2.min.js b/app/rollerverbrauch/static/modernizr-2.8.3-respond-1.4.2.min.js similarity index 100% rename from app/static/modernizr-2.8.3-respond-1.4.2.min.js rename to app/rollerverbrauch/static/modernizr-2.8.3-respond-1.4.2.min.js diff --git a/app/static/normalize.min.css b/app/rollerverbrauch/static/normalize.min.css similarity index 100% rename from app/static/normalize.min.css rename to app/rollerverbrauch/static/normalize.min.css diff --git a/app/templates/account.html b/app/rollerverbrauch/templates/account.html similarity index 100% rename from app/templates/account.html rename to app/rollerverbrauch/templates/account.html diff --git a/app/templates/admin.html b/app/rollerverbrauch/templates/admin.html similarity index 100% rename from app/templates/admin.html rename to app/rollerverbrauch/templates/admin.html diff --git a/app/templates/layout.html b/app/rollerverbrauch/templates/layout.html similarity index 100% rename from app/templates/layout.html rename to app/rollerverbrauch/templates/layout.html diff --git a/app/templates/manual.html b/app/rollerverbrauch/templates/manual.html similarity index 100% rename from app/templates/manual.html rename to app/rollerverbrauch/templates/manual.html diff --git a/app/templates/newPitStopForm.html b/app/rollerverbrauch/templates/newPitStopForm.html similarity index 100% rename from app/templates/newPitStopForm.html rename to app/rollerverbrauch/templates/newPitStopForm.html diff --git a/app/templates/pitstops.html b/app/rollerverbrauch/templates/pitstops.html similarity index 100% rename from app/templates/pitstops.html rename to app/rollerverbrauch/templates/pitstops.html diff --git a/app/templates/security/change_password.html b/app/rollerverbrauch/templates/security/change_password.html similarity index 100% rename from app/templates/security/change_password.html rename to app/rollerverbrauch/templates/security/change_password.html diff --git a/app/templates/security/forgot_password.html b/app/rollerverbrauch/templates/security/forgot_password.html similarity index 100% rename from app/templates/security/forgot_password.html rename to app/rollerverbrauch/templates/security/forgot_password.html diff --git a/app/templates/security/login_user.html b/app/rollerverbrauch/templates/security/login_user.html similarity index 100% rename from app/templates/security/login_user.html rename to app/rollerverbrauch/templates/security/login_user.html diff --git a/app/templates/security/register_user.html b/app/rollerverbrauch/templates/security/register_user.html similarity index 100% rename from app/templates/security/register_user.html rename to app/rollerverbrauch/templates/security/register_user.html diff --git a/app/templates/security/reset_password.html b/app/rollerverbrauch/templates/security/reset_password.html similarity index 100% rename from app/templates/security/reset_password.html rename to app/rollerverbrauch/templates/security/reset_password.html diff --git a/app/templates/statistics.html b/app/rollerverbrauch/templates/statistics.html similarity index 100% rename from app/templates/statistics.html rename to app/rollerverbrauch/templates/statistics.html diff --git a/app/schema.sql b/app/schema.sql deleted file mode 100644 index c280175..0000000 --- a/app/schema.sql +++ /dev/null @@ -1,16 +0,0 @@ -drop table if exists pitstops; -create table pitstops ( - `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - `date` TEXT NOT NULL, - `odometer` INTEGER NOT NULL, - `litres` REAL NOT NULL -); - -drop table if exists users; -create table `users` ( - `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - `name` TEXT NOT NULL, - `salt` TEXT NOT NULL, - `password` TEXT NOT NULL -); -insert into users (name, salt, password) values ('shing19m', 'pL85Kl2U', '207357fdbf6f379c53bb5ab7fa0bc8c0072ae743973a510f551db7b5c90049b7'); diff --git a/create_icons.sh b/create_icons.sh index ddabb26..af41ca8 100755 --- a/create_icons.sh +++ b/create_icons.sh @@ -4,14 +4,14 @@ SIZES="57 60 72 76 114 120 144 152 180" for SIZE in $SIZES do - convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/static/apple-touch-icon-${SIZE}.png + convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/rollerverbrauch/static/apple-touch-icon-${SIZE}.png done -convert ./icon_orig.png -resize 192x192 app/static/android-icon-192x192.png +convert ./icon_orig.png -resize 192x192 app/rollerverbrauch/static/android-icon-192x192.png SIZES="16 32 96" for SIZE in $SIZES do - convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/static/favicon-${SIZE}.png + convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/rollerverbrauch/static/favicon-${SIZE}.png done