diff --git a/README.md b/README.md index 442cb08..afe2bdf 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ reloaded automatically. The sqlite file will be stored in *tmp* so it can be inspected with tools like *sqlite3*. The switch *DEBUG* enables debugging during development. -`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -e config=../config/config.py --link pitstops_db:database -p 5000:5000 rollerverbrauch` - +`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app --link pitstops_db:database -p 5000:5000 -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch` ## run in production `docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch` diff --git a/app/__init__.py b/app/__init__.py index d34f7ac..58a6b80 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -12,6 +12,8 @@ from flask_security.forms import LoginForm from flask_sqlalchemy import SQLAlchemy import os from config import config +from sqlalchemy.exc import IntegrityError +from flask.ext.security.forms import LoginForm from .forms import \ CreatePitstopForm, \ @@ -20,8 +22,12 @@ from .forms import \ SelectVehicleForm, \ DeleteAccountForm, \ DeletePitStopForm, \ - EditPitstopForm -from .tools import * + EditPitstopForm, \ + CreateConsumableForm, \ + EditConsumableForm, \ + DeletConsumableForm, \ + SelectConsumableForm + app = Flask(__name__) app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default']) @@ -33,10 +39,12 @@ from .entities import \ User, \ Role, \ Pitstop, \ - Vehicle -from .filters import * -#import rollerverbrauch.tools as tools + Vehicle, \ + Consumable + # required to activate the filters +from .filters import * +from .tools import * user_datastore = SQLAlchemyUserDatastore(db, User, Role) @@ -82,12 +90,12 @@ def index(): kilometers = 0 for vehicle in vehicles: stats = tools.VehicleStats(vehicle) - litres += stats.overall_litres + #litres += stats.overall_litres kilometers += stats.overall_distance vehicle_count = len(vehicles) pitstop_count = len(Pitstop.query.all()) data = { - 'users':user_count, + 'users': user_count, 'vehicles': vehicle_count, 'pitstops': pitstop_count, 'litres': litres, @@ -96,26 +104,47 @@ def index(): return render_template('index.html', login_user_form=LoginForm(), data=data) -@app.route('/account/edit_vehicle/', methods=['GET', 'POST']) +@app.route('/account/vehicle/edit/', methods=['GET', 'POST']) @login_required def edit_vehicle(vid): vehicle = Vehicle.query.filter(Vehicle.id == vid).first() + + # prevent edit of foreign vehicles + if vehicle not in current_user.vehicles: + return redirect(url_for('get_account_page')) + form = EditVehicleForm() + form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()] + + if not form.consumables.data: + form.consumables.default = [g.id for g in vehicle.consumables] + + if form.name.data is not None: + form.name.default = form.name.data if form.validate_on_submit(): - if not tools.check_vehicle_name_is_unique(current_user, form.name): - return render_template('editVehicleForm.html', form=form) vehicle.name = form.name.data - db.session.commit() - tools.db_log_update(vehicle) + # we cannot delete consumables where there are pitstops for => report error + vehicle.consumables = [] + for consumable_id in form.consumables.data: + consumable = Consumable.query.get(consumable_id) + if consumable is not None: + vehicle.consumables.append(consumable) + try: + db.session.commit() + tools.db_log_update(vehicle) + except IntegrityError: + db.session.rollback() + form.name.errors.append('"%s" is not unique.' % (form.name.data)) + return render_template('editVehicleForm.html', form=form) return redirect(url_for('get_account_page')) form.name.default = vehicle.name form.process() - return render_template('editVehicleForm.html', form=form) + return render_template('editVehicleForm.html', form=form, vehicle=vehicle) -@app.route('/account/delete_vehicle/', methods=['GET', 'POST']) +@app.route('/account/vehicle/delete/', methods=['GET', 'POST']) @login_required def delete_vehicle(vid): vehicle = Vehicle.query.filter(Vehicle.id == vid).first() @@ -124,6 +153,9 @@ def delete_vehicle(vid): if vehicle not in current_user.vehicles: return redirect(url_for('get_account_page')) + if len(current_user.vehicles) == 1: + return redirect(url_for('get_account_page')) + form = DeleteVehicleForm() if form.validate_on_submit(): @@ -135,75 +167,124 @@ def delete_vehicle(vid): return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle) -@app.route('/account/create_vehicle', methods=['GET', 'POST']) +@app.route('/account/vehicle/create', methods=['GET', 'POST']) @login_required def create_vehicle(): form = EditVehicleForm() + form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()] + + if form.name.data is not None: + form.name.default = form.name.data + + if form.consumables.data: + form.consumables.default = form.consumables.data if form.validate_on_submit(): - vehicle_name = form.name.data - if not tools.check_vehicle_name_is_unique(current_user, form.name): + if len(form.consumables.data) == 0: + form.consumables.errors.append('At least one consumable must be selected.') return render_template('createVehicleForm.html', form=form) + + vehicle_name = form.name.data new_vehicle = Vehicle(vehicle_name) + for consumable_id in form.consumables.data: + consumable = Consumable.query.get(consumable_id) + if consumable is not None: + new_vehicle.consumables.append(consumable) db.session.add(new_vehicle) current_user.vehicles.append(new_vehicle) - db.session.commit() - tools.db_log_add(new_vehicle) + try: + db.session.commit() + tools.db_log_add(new_vehicle) + except IntegrityError: + db.session.rollback() + form.name.errors.append('"%s" is not unique.' % (form.name.data)) + return render_template('createVehicleForm.html', form=form) return redirect(url_for('get_account_page')) return render_template('createVehicleForm.html', form=form) -@app.route('/pitstops/select_vehicle', methods=['GET', 'POST']) +@app.route('/pitstops/vehicle/select', methods=['GET', 'POST']) @login_required def select_vehicle_for_new_pitstop(): + if len(current_user.vehicles) == 1: + return redirect(url_for('select_consumable_for_new_pitstop', vid=current_user.vehicles[0].id)) + 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('select_consumable_for_new_pitstop', vid=form.vehicle.data)) - return redirect(url_for('create_pit_stop_form', vid=form.vehicle.data)) - - return render_template('selectVehice.html', form=form) + return render_template('selectVehicle.html', form=form) -@app.route('/pitstops/create/', methods=['GET', 'POST']) +@app.route('/pitstops/vehicle//consumable/select', 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: +def select_consumable_for_new_pitstop(vid): + vehicle = Vehicle.query.get(vid) + if vehicle is None or 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(1970, 1, 1), 0) + if len(vehicle.consumables) == 1: + return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id)) - form = CreatePitstopForm() - form.set_pitstop(last_pitstop) + form = SelectConsumableForm() + form.consumable.choices = [(g.id, g.name) for g in vehicle.consumables] if form.validate_on_submit(): - new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data, form.costs.data) + return redirect(url_for('create_pit_stop_form', vid=vid, cid=form.consumable.data)) + + return render_template('selectConsumableForVehicle.html', vehicle=vehicle, form=form) + + +@app.route('/pitstops/vehicle//consumable//create', methods=['GET', 'POST']) +@login_required +def create_pit_stop_form(vid, cid): + vehicle = Vehicle.query.get(vid) + if vehicle is None or vehicle not in current_user.vehicles: + return redirect(url_for('select_vehicle_for_new_pitstop')) + + consumable = Consumable.query.get(cid) + if consumable not in vehicle.consumables: + return redirect(url_for('select_consumable_for_new_pitstop', vid=vid)) + + form = CreatePitstopForm() + + # the last pitstop is required to be able to check the monotonicy of date and odometer + last_pitstop = tools.get_latest_pitstop_for_vehicle(vid) + last_pitstop_consumable = tools.get_latest_pitstop_for_vehicle_and_consumable(vid, cid) + + # we can enter the same odometer if the pitstops are not equal + form.same_odometer_allowed = (last_pitstop != last_pitstop_consumable) + + # set the lower limits for odometer andd date and the values for amount and costs of the last stop + form.set_pitstop(tools.compute_lower_limits_for_new_pitstop(last_pitstop, last_pitstop_consumable, cid)) + + # set the label of the litres field to make the user comfortable + form.set_consumable(consumable) + + # preinitialize the defaults with potentially existing values from a try before + form.preinit_with_data() + + # + # Validate should accept same odometer on different consumables + # + if form.validate_on_submit(): + new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data, form.costs.data, cid) db.session.add(new_stop) vehicle.pitstops.append(new_stop) - db.session.commit() - tools.db_log_add(new_stop) + try: + db.session.commit() + tools.db_log_add(new_stop) + except IntegrityError: + db.session.rollback() + form.odometer.errors.append('Pitstop already present for %s at odometer %s km!' % (consumable.name, form.odometer.data)) + return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) 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.costs.default = last_pitstop.costs form.process() - messages = { - 'date': 'Date must be between %s and %s (including).' % (str(last_pitstop.date), str(date.today())), - 'odometer': 'Odometer must be greater than %s km.' % (str(last_pitstop.odometer)), - 'costs': 'Costs must be higher than 0.01 €.' - } - return render_template('newPitStopForm.html', form=form, vehicle=vehicle, messages = messages) + return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) @app.route('/pitstops/delete/', methods=['GET', 'POST']) @@ -229,9 +310,10 @@ def delete_pit_stop_form(pid): @app.route('/pitstops/edit/', methods=['GET', 'POST']) @login_required def edit_pit_stop_form(pid): - edit_pitstop = Pitstop.query.filter(Pitstop.id == pid).first() + edit_pitstop = Pitstop.query.get(pid).first() if edit_pitstop is None: return redirect(url_for('get_pit_stops')) + vehicle = Vehicle.query.filter(Vehicle.id == edit_pitstop.vehicle_id).first() if vehicle not in current_user.vehicles: return redirect(url_for('get_pit_stops')) @@ -240,7 +322,7 @@ def edit_pit_stop_form(pid): if last_pitstop_pos > 0: last_pitstop = vehicle.pitstops[last_pitstop_pos] else: - last_pitstop = Pitstop(0, 0, date(1970, 1, 1)) + last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) form = EditPitstopForm() form.set_pitstop(last_pitstop) @@ -283,8 +365,88 @@ def get_manual(): @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) + users = User.query.all() + consumables = Consumable.query.all() + for consumable in consumables: + consumable.in_use = len(consumable.vehicles) > 0 + return render_template('admin.html', users=users, consumables=consumables) + + +@app.route('/admin/consumable/create', methods=['GET', 'POST']) +@login_required +def create_consumable(): + form = CreateConsumableForm() + + # preinitialize the defaults with potentially existing values from a try before + if form.name.data is not None: + form.name.default = form.name.data + if form.unit.data is not None: + form.unit.default = form.unit.data + + if form.validate_on_submit(): + new_consumable = Consumable(form.name.data, form.unit.data) + db.session.add(new_consumable) + try: + db.session.commit() + tools.db_log_add(new_consumable) + except IntegrityError: + db.session.rollback() + form.name.errors.append('"%s" is not unique.' % (form.name.data)) + return render_template('createConsumableForm.html', form=form) + return redirect(url_for('get_admin_page')) + + return render_template('createConsumableForm.html', form=form) + + +@app.route('/admin/consumable/delete/', methods=['GET', 'POST']) +@login_required +def delete_consumable(cid): + consumable = Consumable.query.filter(Consumable.id == cid).first() + if consumable is None: + return redirect(url_for('get_admin_page')) + + form = DeletConsumableForm() + + if form.validate_on_submit(): + db.session.delete(consumable) + db.session.commit() + tools.db_log_delete(consumable) + return redirect(url_for('get_admin_page')) + + return render_template('deleteConsumableForm.html', form=form, consumable=consumable) + + +@app.route('/admin/consumable/edit/', methods=['GET', 'POST']) +@login_required +def edit_consumable(cid): + consumable = Consumable.query.filter(Consumable.id == cid).first() + if consumable is None: + return redirect(url_for('get_admin_page')) + + form = EditConsumableForm() + + form.name.default = consumable.name + form.unit.default = consumable.unit + + # preinitialize the defaults with potentially existing values from a try before + if form.name.data is not None: + form.name.default = form.name.data + if form.unit.data is not None: + form.unit.default = form.unit.data + + if form.validate_on_submit(): + consumable.name = form.name.data + consumable.unit = form.unit.data + try: + db.session.commit() + tools.db_log_update(consumable) + except IntegrityError: + db.session.rollback() + form.name.errors.append('"%s" is not unique.' % (form.name.data)) + return render_template('editConsumableForm.html', form=form) + return redirect(url_for('get_admin_page')) + + return render_template('editConsumableForm.html', form=form) @app.route('/account', methods=['GET']) diff --git a/app/entities.py b/app/entities.py index 508513b..5e06001 100644 --- a/app/entities.py +++ b/app/entities.py @@ -5,8 +5,15 @@ 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'))) +vehicles_consumables = db.Table('vehicles_consumables', + db.Column('vehicle_id', db.Integer(), db.ForeignKey('vehicle.id')), + db.Column('consumable_id', db.Integer(), db.ForeignKey('consumable.id'))) + class Role(db.Model, RoleMixin): + """ + Entity to handle different roles for users: Typically user and admin exist + """ id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(80), unique=True) description = db.Column(db.String(255)) @@ -19,11 +26,15 @@ class Role(db.Model, RoleMixin): class User(db.Model, UserMixin): + """ + Entity to represent a user including login data and links to roles and vehicles. + """ 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' ) @@ -38,13 +49,28 @@ class User(db.Model, UserMixin): class Vehicle(db.Model): + """ + Entity to represent a vehicle. + Attributes: + * name of the vehilce + * the id of the owner + * list of pitstops + * list of possible consumables + """ id = db.Column(db.Integer, primary_key=True) owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) name = db.Column(db.String(255)) pitstops = db.relationship( 'Pitstop' ) - __table_args__ = (db.UniqueConstraint('owner_id', 'name', name='_owner_name_uniq'),) + consumables = db.relationship( + 'Consumable', + secondary=vehicles_consumables + ) + # allow vehicle names to be duplicated between different owners but must still be uniq for each owner + __table_args__ = (db.UniqueConstraint('owner_id', + 'name', + name='_owner_name_uniq'),) def __init__(self, name): self.name = name @@ -54,18 +80,63 @@ class Vehicle(db.Model): class Pitstop(db.Model): + """ + Entity to represent a pitstop for a single consumable. + Attributes: + * the date of the pitstop + * the odometer of the pitstop + * the id of the fuelled consumable + * amount of consumable used + * the costs of the consumable + * the id of the vehicle that was refuelled + """ 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)) + consumable_id = db.Column(db.Integer, db.ForeignKey('consumable.id')) + amount = db.Column(db.Numeric(5, 2)) costs = db.Column(db.Numeric(5, 2), default=0) vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id')) + # short cut to access the fuelled consumable of the pitstop + consumable = db.relationship('Consumable') + # this uniqueness constraint makes sure that for each consumable and each vehicle only one pitstop exists at the + # same odometer + __table_args__ = (db.UniqueConstraint('odometer', + 'consumable_id', + 'vehicle_id', + name='_odometer_consumable_vehicle_uniq'),) - def __init__(self, odometer, litres, date, costs): + def __init__(self, odometer, amount, date, costs, consumable_id): self.odometer = odometer - self.litres = litres + self.amount = amount self.date = date self.costs = costs + self.consumable_id = consumable_id def __repr__(self): - return '' % (self.odometer, self.litres, self.date, self.vehicle_id) \ No newline at end of file + return '' % \ + (self.odometer, self.amount, self.date, self.vehicle_id, self.consumable_id) + + +class Consumable(db.Model): + """ + Entity to represent a material that be consumed by a vehilce. + Attributes: + * name (must be globally unique) + * unit + """ + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), unique=True) + unit = db.Column(db.String(255)) + + vehicles = db.relationship( + 'Vehicle', + secondary=vehicles_consumables + ) + + def __init__(self, name, unit): + self.name = name + self.unit = unit + + def __repr__(self): + return '' % (self.name, self.unit) diff --git a/app/forms.py b/app/forms.py index 5fb1801..f884643 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,10 +1,17 @@ from flask_wtf import Form -from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField +from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField, SelectMultipleField, BooleanField from wtforms.validators import ValidationError, Length from datetime import date def date_check(form, field): + """ + Checks that the date of the pitstop is not before the date of the latest pitstop and not after today. + + :param form: the form where the field is in + :param field: the field to check + :return: Nothing or a ValidationError if the limits are not kept + """ if field.data < form.last_pitstop.date: raise ValidationError('The new date must not be before %s' % form.last_pitstop.date) if field.data > date.today(): @@ -12,7 +19,15 @@ def date_check(form, field): def odometer_check(form, field): - if field.data <= form.last_pitstop.odometer: + """ + + :param form: + :param field: + :return: + """ + if not form.same_odometer_allowed and field.data <= form.last_pitstop.odometer: + raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer) + if form.same_odometer_allowed and field.data < form.last_pitstop.odometer: raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer) @@ -27,7 +42,13 @@ def costs_check(form, field): def edit_costs_check(form, field): - costs_check_required = (form.costs.default is not None and form.costs.default > 0) + """ + Costs must be given, if a default value was given to the form field. + :param form: + :param field: + :return: + """ + costs_check_required = (form.costs.default is not None and form.costs.default > 0) if costs_check_required and field.data is not None and field.data <= 0: raise ValidationError('Costs must be above 0.01 €.') @@ -37,6 +58,11 @@ class SelectVehicleForm(Form): submit = SubmitField(label='Do it!') +class SelectConsumableForm(Form): + consumable = SelectField('Consumable', 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]) @@ -44,13 +70,48 @@ class CreatePitstopForm(Form): costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) submit = SubmitField(label='Do it!') last_pitstop = None + same_odometer_allowed = True def set_pitstop(self, last_pitstop): self.last_pitstop = last_pitstop + def set_consumable(self, consumable): + self.litres.label = '%s (%s)' % (consumable.name, consumable.unit) + + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + else: + self.date.default = self.last_pitstop.date + if self.odometer.data: + self.odometer.default = self.odometer.data + else: + self.odometer.default = self.last_pitstop.odometer + if self.litres.data: + self.litres.default = self.litres.data + else: + self.litres.default = self.last_pitstop.amount + if self.costs.data: + self.costs.default = self.costs.data + else: + self.costs.default = self.last_pitstop.costs + + def get_hint_messages(self): + if self.same_odometer_allowed: + or_equal = ' or equal to' + else: + or_equal = '' + messages = { + 'date': 'Date must be between %s and %s (including).' % (str(self.last_pitstop.date), str(date.today())), + 'odometer': 'Odometer must be greater than%s %s km.' % (or_equal, str(self.last_pitstop.odometer)), + 'costs': 'Costs must be higher than 0.01 €.' + } + return messages + class EditVehicleForm(Form): name = StringField('Name', validators=[Length(1, 255)]) + consumables = SelectMultipleField('Consumables', coerce=int,validators=[]) submit = SubmitField(label='Do it!') @@ -73,8 +134,36 @@ class EditPitstopForm(Form): costs = DecimalField('Costs (€, overall)', places=2, validators=[edit_costs_check]) submit = SubmitField(label='Update it!') last_pitstop = None + same_odometer_allowed = True def set_pitstop(self, last_pitstop): self.last_pitstop = last_pitstop + def set_consumable(self, consumable): + self.litres.label = '%s (%s)' % (consumable.name, consumable.unit) + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + if self.odometer.data: + self.odometer.default = self.odometer.data + if self.litres.data: + self.litres.default = self.litres.data + if self.costs.data: + self.costs.default = self.costs.data + + +class CreateConsumableForm(Form): + name = StringField('Name', validators=[Length(1, 255)]) + unit = StringField('Unit', validators=[Length(1, 255)]) + submit = SubmitField(label='Do it!') + + +class EditConsumableForm(Form): + name = StringField('Name', validators=[Length(1, 255)]) + unit = StringField('Unit', validators=[Length(1, 255)]) + submit = SubmitField(label='Do it!') + + +class DeletConsumableForm(Form): + submit = SubmitField(label='Do it!') diff --git a/app/templates/account.html b/app/templates/account.html index cc1ed3a..43a0d33 100644 --- a/app/templates/account.html +++ b/app/templates/account.html @@ -36,7 +36,8 @@ {{ vehicle.name }} - {{ vehicle.pitstops | length }} pitstops + {{ vehicle.pitstops | length }} pitstops
+ {{ vehicle.consumables | length }} consumables diff --git a/app/templates/admin.html b/app/templates/admin.html index 8b25d30..8bee9ef 100644 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -1,18 +1,66 @@ {% extends "layout.html" %} {% block body %} -
-
+

Admin

+
Users
-

Admin

- We have {{ data.users|length }} users so far: + We have {{ users|length }} users so far:
    - {% for user in data.users %} + {% for user in users %}
  • {{user.email}}
  • {% endfor %} -
-
+ +
+
Consumables
+
+ + + + + + + + + {% for consumable in consumables %} + + + + + + + {% endfor %} + +
+ Name + + Unit + + Used by + + Actions +
+ {{ consumable.name }} + + {{ consumable.unit }} + + {{ consumable.vehicles | length }} vehicles + + {% if not consumable.in_use %} + + delete + + {% endif %} + + edit + +
+
+ {% endblock %} diff --git a/app/templates/createConsumableForm.html b/app/templates/createConsumableForm.html new file mode 100644 index 0000000..1634ccf --- /dev/null +++ b/app/templates/createConsumableForm.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

Create consumable

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.unit) }} + {{ render_field_with_errors(form.submit) }} +
+
+
+
+
+{% endblock %} diff --git a/app/templates/newPitStopForm.html b/app/templates/createPitStopForm.html similarity index 97% rename from app/templates/newPitStopForm.html rename to app/templates/createPitStopForm.html index feebbec..a6941e3 100644 --- a/app/templates/newPitStopForm.html +++ b/app/templates/createPitStopForm.html @@ -26,4 +26,5 @@ +
{% endblock %} diff --git a/app/templates/createVehicleForm.html b/app/templates/createVehicleForm.html index 2dce35b..9e5ced5 100644 --- a/app/templates/createVehicleForm.html +++ b/app/templates/createVehicleForm.html @@ -9,6 +9,7 @@
{{ form.hidden_tag() }} {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.consumables) }} {{ render_field_with_errors(form.submit) }}
diff --git a/app/templates/deleteConsumableForm.html b/app/templates/deleteConsumableForm.html new file mode 100644 index 0000000..40a6e88 --- /dev/null +++ b/app/templates/deleteConsumableForm.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

Delete vehicle '{{consumable.name}}'?

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

Edit consumable

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.unit) }} + {{ render_field_with_errors(form.submit) }} +
+
+
+
+
+{% endblock %} diff --git a/app/templates/editVehicleForm.html b/app/templates/editVehicleForm.html index 8d2827f..c5b18dd 100644 --- a/app/templates/editVehicleForm.html +++ b/app/templates/editVehicleForm.html @@ -9,6 +9,7 @@
{{ form.hidden_tag() }} {{ render_field_with_errors(form.name) }} + {{ render_field_with_errors(form.consumables) }} {{ render_field_with_errors(form.submit) }}
diff --git a/app/templates/layout.html b/app/templates/layout.html index 60e5ebe..8920020 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -85,30 +85,6 @@ {% endmacro %} -{% macro chartScript(divId, data, unit)%} - {% set hash = divId | md5 %} - - data_{{ hash }} = [{% for stop in data %}{ - "date": "{{stop.date}}", - "value": {{stop.value}} - }{% if not loop.last %},{%endif%} - {% endfor%} - ] - var chart_{{ hash }} = createChart('{{divId}}', data_{{ hash }}, '{{unit}}'); - - function zoom_chart_{{ hash }}() { - chart_{{ hash }}.zoomToIndexes( - chart_{{ hash }}.dataProvider.length - 40, - chart_{{ hash }}.dataProvider.length - 1 - ); - } - - chart_{{ hash }}.addListener("rendered", zoom_chart_{{ hash }}); - - zoom_chart_{{ hash }}() - -{% endmacro %} - diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html index f45c131..25c7283 100644 --- a/app/templates/pitstops.html +++ b/app/templates/pitstops.html @@ -1,6 +1,7 @@ {% extends "layout.html" %} {% block body %} -
+
+
+ {% endfor %} {% else %} {% endif %}
{% endfor %} +
+ {% else %} + + {% endif %} +{% endmacro %} + +{% macro print_consumable_table(consumable) %} + + + + + + + + + + + + + + + + + +
Average Distance:{{ consumable.average_distance | round(2) }} km
Amount fuelled:{{ consumable.overall_amount | round(2) }} {{ consumable.unit }}
Average Amount fuelled:{{ consumable.average_amount_fuelled | round(2) }} {{ consumable.unit }}
Average Amount used:{{ consumable.average_amount_used | round(2) }} {{ consumable.unit }}/100km
+{% endmacro %} + +{% macro print_vehicle_table(vehicle) %} +

{{vehicle.name}}

+
+ + + + + + + + + +
Logged Distance:{{ vehicle.overall_distance | round(2) }} km
Logged Costs:{{ vehicle.overall_costs | round(2) }} €
+
+{% endmacro %} + +{% macro tab_script(id) %} + +{% endmacro %} + {% 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
Logged Costs:{{ vehicle.overall_costs | round(2) }} €
Average Costs per Litre:{{ vehicle.average_costs_per_litre | round(2) }} €/l
-
-