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..e2eea31 --- /dev/null +++ b/app/rollerverbrauch/__init__.py @@ -0,0 +1,209 @@ +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.tools import \ + VehicleStats + +from rollerverbrauch.forms import \ + CreatePitstopForm, \ + EditVehicleForm, \ + DeleteVehicleForm, \ + SelectVehicleForm + +from rollerverbrauch.entities import \ + User, \ + Role, \ + Pitstop, \ + Vehicle + +# required to activate the filters +import rollerverbrauch.filters + + +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) + v = Vehicle('default vehicle') + db.session.add(v) + user.vehicles.append(v) + db.session.commit() + + +@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('/account/edit_vehicle/', methods=['GET', 'POST']) +@login_required +def edit_vehicle(vid): + vehicle = Vehicle.query.filter(Vehicle.id == vid).first() + form = EditVehicleForm() + + if form.validate_on_submit(): + vehicle.name = form.name.data + db.session.commit() + return redirect(url_for('get_account_page')) + + form.name.default = vehicle.name + form.process() + return render_template('editVehicleForm.html', form=form) + + +@app.route('/account/delete_vehicle/', methods=['GET', 'POST']) +@login_required +def delete_vehicle(vid): + vehicle = Vehicle.query.filter(Vehicle.id == vid).first() + + # prevent deletion of foreign vehicles + if vehicle not in current_user.vehicles: + return redirect(url_for('get_account_page')) + + form = DeleteVehicleForm() + + if form.validate_on_submit(): + db.session.delete(vehicle) + db.session.commit() + return redirect(url_for('get_account_page')) + + return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle) + + +@app.route('/account/create_vehicle', methods=['GET', 'POST']) +@login_required +def create_vehicle(): + form = EditVehicleForm() + + if form.validate_on_submit(): + new_vehicle = Vehicle(form.name.data) + db.session.add(new_vehicle) + current_user.vehicles.append(new_vehicle) + db.session.commit() + return redirect(url_for('get_account_page')) + + return render_template('createVehicleForm.html', form=form) + + +@app.route('/pitstops/select_vehicle', methods=['GET', 'POST']) +@login_required +def select_vehicle_for_new_pitstop(): + form = SelectVehicleForm() + form.vehicle.choices = [(g.id, g.name) for g in current_user.vehicles] + + if form.validate_on_submit(): + vehicle = Vehicle.query.filter(Vehicle.id == form.vehicle.data).first() + if vehicle not in current_user.vehicles: + return render_template('selectVehice.html', form=form) + + return redirect(url_for('create_pit_stop_form', vid=form.vehicle.data)) + + return render_template('selectVehice.html', form=form) + + +@app.route('/pitstops/create/', methods=['GET', 'POST']) +@login_required +def create_pit_stop_form(vid): + vehicle = Vehicle.query.filter(Vehicle.id == vid).first() + if vehicle not in current_user.vehicles: + return redirect(url_for('select_vehicle_for_new_pitstop')) + + if len(vehicle.pitstops) > 0: + last_pitstop = vehicle.pitstops[-1] + else: + 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) + vehicle.pitstops.append(new_stop) + db.session.commit() + return redirect(url_for('get_pit_stops', _anchor= 'v' + str(vehicle.id))) + + 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, vehicle = vehicle) + + +@app.route('/pitstops', methods=['GET']) +@login_required +def get_pit_stops(): + return render_template('pitstops.html', user=current_user) + + +@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(): + stats = [] + for vehicle in current_user.vehicles: + stats.append(VehicleStats(vehicle)) + return render_template('statistics.html', data=stats) + 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..8c40b1c --- /dev/null +++ b/app/rollerverbrauch/entities.py @@ -0,0 +1,68 @@ +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()) + vehicles = db.relationship( + 'Vehicle' + ) + roles = db.relationship( + 'Role', + secondary=roles_users, + backref=db.backref('users', lazy='dynamic') + ) + + def __repr__(self): + return '' % self.email + + +class Vehicle(db.Model): + id = db.Column(db.Integer, primary_key=True) + owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) + name = db.Column(db.String(255), unique=True) + pitstops = db.relationship( + 'Pitstop' + ) + + def __init__(self, name): + self.name = name + + def __repr__(self): + return '' % self.name + + +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)) + vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id')) + + def __init__(self, odometer, litres, date): + self.odometer = odometer + self.litres = litres + self.date = date + + def __repr__(self): + return '' % (self.odometer, self.litres) \ No newline at end of file diff --git a/app/rollerverbrauch/filters.py b/app/rollerverbrauch/filters.py new file mode 100644 index 0000000..00a0e59 --- /dev/null +++ b/app/rollerverbrauch/filters.py @@ -0,0 +1,9 @@ +from rollerverbrauch import app + + +@app.template_filter('none_filter') +def none_filter(value): + if value is None: + return '' + else: + return value diff --git a/app/rollerverbrauch/forms.py b/app/rollerverbrauch/forms.py new file mode 100644 index 0000000..2f2b87f --- /dev/null +++ b/app/rollerverbrauch/forms.py @@ -0,0 +1,43 @@ +from flask_wtf import Form +from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField +from wtforms.validators import ValidationError, Length + + +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 SelectVehicleForm(Form): + vehicle = SelectField('Vehicle', coerce=int) + submit = SubmitField(label='Do it!') + + +class CreatePitstopForm(Form): + date = DateField('Date of Pitstop', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) + submit = SubmitField(label='Do it!') + pitstop = None + + def set_pitstop(self, pitstop): + self.pitstop = pitstop + + +class EditVehicleForm(Form): + name = StringField('Name', validators=[Length(1, 255)]) + submit = SubmitField(label='Do it!') + + +class DeleteVehicleForm(Form): + submit = SubmitField(label='Do it!') 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/rollerverbrauch/templates/account.html b/app/rollerverbrauch/templates/account.html new file mode 100644 index 0000000..8cec4de --- /dev/null +++ b/app/rollerverbrauch/templates/account.html @@ -0,0 +1,56 @@ +{% extends "layout.html" %} + +{% block body %} +

Account management for {{current_user.email}}

+
+
Password
+ +
+
+
Vehicles
+ + + + + + + + {% for vehicle in current_user.vehicles %} + + + + + + {% endfor %} +
+ Vehicle + + Info + + Actions +
+ {{ vehicle.name }} + + {{ vehicle.pitstops | length }} pitstops + + + edit + + {% if current_user.vehicles | length > 1 %} + + delete + + {% else %} +   + {% endif %} +
+
+{% endblock %} diff --git a/app/templates/admin.html b/app/rollerverbrauch/templates/admin.html similarity index 94% rename from app/templates/admin.html rename to app/rollerverbrauch/templates/admin.html index 97c2db5..d57f4da 100644 --- a/app/templates/admin.html +++ b/app/rollerverbrauch/templates/admin.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %} -

Admin

+

Admin

We have {{ data.users|length }} users so far:
    {% for user in data.users %} diff --git a/app/rollerverbrauch/templates/createVehicleForm.html b/app/rollerverbrauch/templates/createVehicleForm.html new file mode 100644 index 0000000..4ea3b84 --- /dev/null +++ b/app/rollerverbrauch/templates/createVehicleForm.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} + +{% block body %} +

    Create vehicle

    +
    + {{ form.hidden_tag() }} + {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.submit) }} +
    + + +{% endblock %} diff --git a/app/rollerverbrauch/templates/deleteVehicleForm.html b/app/rollerverbrauch/templates/deleteVehicleForm.html new file mode 100644 index 0000000..5e957c3 --- /dev/null +++ b/app/rollerverbrauch/templates/deleteVehicleForm.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block body %} +

    Delete vehicle '{{vehicle.name}}'

    +
    + {{ form.hidden_tag() }} + {{ render_field_with_errors(form.submit) }} +
    + + +{% endblock %} diff --git a/app/rollerverbrauch/templates/editVehicleForm.html b/app/rollerverbrauch/templates/editVehicleForm.html new file mode 100644 index 0000000..627a24f --- /dev/null +++ b/app/rollerverbrauch/templates/editVehicleForm.html @@ -0,0 +1,12 @@ +{% extends "layout.html" %} + +{% block body %} +

    Edit vehicle

    +
    + {{ form.hidden_tag() }} + {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.submit) }} +
    + + +{% endblock %} diff --git a/app/templates/layout.html b/app/rollerverbrauch/templates/layout.html similarity index 68% rename from app/templates/layout.html rename to app/rollerverbrauch/templates/layout.html index 29fb991..e44af45 100644 --- a/app/templates/layout.html +++ b/app/rollerverbrauch/templates/layout.html @@ -1,6 +1,6 @@ {% macro navigation() -%} {% if current_user.email %} -
  • Create Pitstop
  • +
  • Create Pitstop
  • Statistics
  • Account
  • {% if current_user.has_role('admin') %} @@ -15,19 +15,46 @@ {% macro render_field_with_errors(field) %}
    - -
    - {{ field(**kwargs)|safe }} - {% if field.errors %} -

    - {% for error in field.errors %} - {{ error }} - {% endfor %} -

    - {% endif %} -
    + {% if field.type == 'SubmitField' %} +
    + +
    + {% else %} + +
    + {% if field.type == 'SelectField' %} + + {% elif field.type == 'BooleanField' %} + + {% elif field.type == 'StringField' %} + + {% elif field.type == 'PasswordField' %} + + {% elif field.type == 'DateField' %} + + {% elif field.type == 'IntegerField' %} + + {% elif field.type == 'DecimalField' %} + + {% else %} + {{ field(**kwargs)|safe }} + {% endif %} + {% if field.errors %} +

    + {% for error in field.errors %} + {{ error }} + {% endfor %} +

    + {% endif %} +
    + {% endif %} +
    {% endmacro %} 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 55% rename from app/templates/newPitStopForm.html rename to app/rollerverbrauch/templates/newPitStopForm.html index c661f10..849432e 100644 --- a/app/templates/newPitStopForm.html +++ b/app/rollerverbrauch/templates/newPitStopForm.html @@ -1,12 +1,13 @@ {% extends "layout.html" %} {% block body %} +

    New Pitstop for '{{ vehicle.name }}'

    - {{ form.hidden_tag() }} - {{ render_field_with_errors(form.date) }} + {{ form.hidden_tag() }} + {{ render_field_with_errors(form.date) }} {{ render_field_with_errors(form.odometer) }} {{ render_field_with_errors(form.litres) }} - + {{ render_field_with_errors(form.submit) }}
    diff --git a/app/rollerverbrauch/templates/pitstops.html b/app/rollerverbrauch/templates/pitstops.html new file mode 100644 index 0000000..0088269 --- /dev/null +++ b/app/rollerverbrauch/templates/pitstops.html @@ -0,0 +1,86 @@ +{% extends "layout.html" %} +{% block body %} +
    + +
    + {% for vehicle in current_user.vehicles %} +
    +

    {{vehicle.name}}

    +
    + + + + + + + {% for pitstop in vehicle.pitstops|reverse %} + {% if not loop.last %} + {% set days = (pitstop.date - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].date).days %} + {% set distance = pitstop.odometer - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].odometer %} + {% set average = (pitstop.litres / distance) * 100 %} + + + + + + {% else %} + + + + + + {% endif %} + {% endfor %} +
    + Date
    + Days +
    + Odometer
    + Distance +
    + Litres
    + Average +
    + {{pitstop.date}}
    + {{ days }} days +
    + {{pitstop.odometer}} km
    + {{distance}} km +
    + {{pitstop.litres}} l
    + {{average | round(2)}} l/100km +
    + {{pitstop.date}}
    + -- days +
    + {{pitstop.odometer}} km
    + -- km +
    + {{pitstop.litres}} l
    + -- l/100km +
    +
    +
    + {% endfor %} +
    +
    + + + +{% endblock %} diff --git a/app/templates/security/change_password.html b/app/rollerverbrauch/templates/security/change_password.html similarity index 89% rename from app/templates/security/change_password.html rename to app/rollerverbrauch/templates/security/change_password.html index 27f5d62..4cc83e1 100644 --- a/app/templates/security/change_password.html +++ b/app/rollerverbrauch/templates/security/change_password.html @@ -8,6 +8,6 @@ {{ 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) }} + {{ render_field_with_errors(change_password_form.submit) }} {% endblock %} diff --git a/app/templates/security/forgot_password.html b/app/rollerverbrauch/templates/security/forgot_password.html similarity index 86% rename from app/templates/security/forgot_password.html rename to app/rollerverbrauch/templates/security/forgot_password.html index 556f254..729b28f 100644 --- a/app/templates/security/forgot_password.html +++ b/app/rollerverbrauch/templates/security/forgot_password.html @@ -6,6 +6,6 @@
    {{ forgot_password_form.hidden_tag() }} {{ render_field_with_errors(forgot_password_form.email) }} - {{ render_field(forgot_password_form.submit) }} + {{ render_field_with_errors(forgot_password_form.submit) }}
    {% endblock %} diff --git a/app/templates/security/login_user.html b/app/rollerverbrauch/templates/security/login_user.html similarity index 91% rename from app/templates/security/login_user.html rename to app/rollerverbrauch/templates/security/login_user.html index 9bc86c2..081f13f 100644 --- a/app/templates/security/login_user.html +++ b/app/rollerverbrauch/templates/security/login_user.html @@ -9,7 +9,7 @@ {{ render_field_with_errors(login_user_form.password) }} {{ render_field_with_errors(login_user_form.remember) }} {{ render_field(login_user_form.next) }} - {{ render_field(login_user_form.submit) }} + {{ render_field_with_errors(login_user_form.submit) }} {% if security.recoverable %} Forgot password {% endif %} diff --git a/app/templates/security/register_user.html b/app/rollerverbrauch/templates/security/register_user.html similarity index 90% rename from app/templates/security/register_user.html rename to app/rollerverbrauch/templates/security/register_user.html index 9cad77d..b269de4 100644 --- a/app/templates/security/register_user.html +++ b/app/rollerverbrauch/templates/security/register_user.html @@ -10,6 +10,6 @@ {% if register_user_form.password_confirm %} {{ render_field_with_errors(register_user_form.password_confirm) }} {% endif %} - {{ render_field(register_user_form.submit) }} + {{ render_field_with_errors(register_user_form.submit) }} {% endblock %} 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/rollerverbrauch/templates/selectVehice.html b/app/rollerverbrauch/templates/selectVehice.html new file mode 100644 index 0000000..f46075d --- /dev/null +++ b/app/rollerverbrauch/templates/selectVehice.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block body %} +

    Select Vehicle

    +
    + {{ form.hidden_tag() }} + {{ render_field_with_errors(form.vehicle) }} + {{ render_field_with_errors(form.submit) }} +
    + +{% endblock %} diff --git a/app/rollerverbrauch/templates/statistics.html b/app/rollerverbrauch/templates/statistics.html new file mode 100644 index 0000000..9fd7b0f --- /dev/null +++ b/app/rollerverbrauch/templates/statistics.html @@ -0,0 +1,62 @@ +{% extends "layout.html" %} + +{% block body %} +
    + +
    + {% for vehicle in data %} +
    +

    {{vehicle.name}}

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Number of Pitstops:{{ vehicle.pitstop_count }}
    Logged Distance:{{ vehicle.overall_distance | round(2) }} km
    Average Distance:{{ vehicle.average_distance | round(2) }} km
    Litres fuelled:{{ vehicle.overall_litres | round(2) }} l
    Average Litres fuelled:{{ vehicle.average_litres_fuelled | round(2) }} l
    Average Litres used:{{ vehicle.average_litres_used | round(2) }} l/100km
    +
    +
    + {% endfor %} +
    +
    + + + + +{% endblock %} diff --git a/app/rollerverbrauch/tools.py b/app/rollerverbrauch/tools.py new file mode 100644 index 0000000..a6281dc --- /dev/null +++ b/app/rollerverbrauch/tools.py @@ -0,0 +1,18 @@ +class VehicleStats: + def __init__(self, vehicle): + self.name = vehicle.name + self.id = vehicle.id + self.pitstop_count = len(vehicle.pitstops) + self.overall_distance = 0 + self.average_distance = 0 + self.overall_litres = 0 + self.average_litres_fuelled = 0 + self.average_litres_used = 0 + if self.pitstop_count > 0: + for pitstop in vehicle.pitstops: + self.overall_litres += pitstop.litres + self.average_litres_fuelled = self.overall_litres / self.pitstop_count + if self.pitstop_count > 1: + self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer + self.average_distance = self.overall_distance / (self.pitstop_count - 1) + self.average_litres_used = 100 * (self.overall_litres - vehicle.pitstops[0].litres) / self.overall_distance 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/app/templates/account.html b/app/templates/account.html deleted file mode 100644 index de46b10..0000000 --- a/app/templates/account.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} -

    Account management for {{current_user.email}}

    - - Change password -{% endblock %} diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html deleted file mode 100644 index 093b226..0000000 --- a/app/templates/pitstops.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} - -
    - - - - - - - {% for pitstop in data['pitstops'] %} - - - - - - {% endfor %} -
    - Date
    - Days -
    - Odometer
    - Distance -
    - Litres
    - Average -
    - {{pitstop.date}}
    - {% if pitstop.days %}{{pitstop.days}}{% else %} --{% endif %} days -
    - {{pitstop.odometer}} km
    - {% if pitstop.distance %}{{pitstop.distance}}{% else %} --{% endif %} km -
    - {{pitstop.litres}} l
    - {% if pitstop.average %}{{pitstop.average | round(2)}}{% else %} --{% endif %} l/100km -
    -
    -{% endblock %} \ No newline at end of file diff --git a/app/templates/statistics.html b/app/templates/statistics.html deleted file mode 100644 index dd75d98..0000000 --- a/app/templates/statistics.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "layout.html" %} - -{% block body %} - -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    Number of Pitstops:{{ data.count }}
    Logged Distance:{{ data.distance | round(2) }} km
    Average Distance:{{ data.averageDistance | round(2) }} km
    Litres fuelled:{{ data.litres | round(2) }} l
    Average Litres fuelled:{{ data.averageListresFuelled | round(2) }} l
    Average Litres used:{{ data.averageListresUsed | round(2) }} l/100km
    -
    -{% endblock %} \ No newline at end of file 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