diff --git a/app/__init__.py b/app/__init__.py index df59df9..26b38cb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,16 +1,11 @@ -from flask import Flask, flash -from flask import redirect, g -from flask import render_template -from flask import url_for +from flask import Flask +from flask import g from flask_mail import Mail -from flask_security import Security, SQLAlchemyUserDatastore, \ - login_required, roles_required, user_registered -from flask_security.core import current_user +from flask_security import Security, SQLAlchemyUserDatastore, user_registered 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 * @@ -22,21 +17,15 @@ app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default']) db = SQLAlchemy(app) mail = Mail(app) -from .entities import \ - User, \ - Role, \ - Pitstop, \ - Vehicle, \ - Consumable, \ - Service +from .entities import * + +user_datastore = SQLAlchemyUserDatastore(db, User, Role) +security = Security(app, user_datastore) # required to activate the filters from .filters import * from .tools import * - - -user_datastore = SQLAlchemyUserDatastore(db, User, Role) -security = Security(app, user_datastore) +from .routes import * @user_registered.connect_via(app) @@ -104,491 +93,3 @@ def index(): return render_template('index.html', login_user_form=LoginForm(), data=data) - -@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(): - vehicle.name = form.name.data - # 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, vehicle=vehicle) - - -@app.route('/account/vehicle/delete/', 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')) - - if len(current_user.vehicles) == 1: - return redirect(url_for('get_account_page')) - - form = DeleteVehicleForm() - - if form.validate_on_submit(): - db.session.delete(vehicle) - db.session.commit() - tools.db_log_delete(vehicle) - return redirect(url_for('get_account_page')) - - return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle) - - -@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 - else: - form.consumables.default = [] - - if form.validate_on_submit(): - 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) - 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/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(): - return redirect(url_for('select_consumable_for_new_pitstop', vid=form.vehicle.data)) - - return render_template('selectVehicle.html', form=form) - - -@app.route('/pitstops/vehicle//consumable/select', methods=['GET', 'POST']) -@login_required -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.consumables) == 0: - flash('Please choose at least one consumable!', 'warning') - return redirect(url_for('edit_vehicle', vid=vid)) - - if len(vehicle.consumables) == 1: - return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id)) - - form = SelectConsumableForm() - form.consumable.choices = [(g.id, g.name) for g in vehicle.consumables] - - if form.validate_on_submit(): - 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) - 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.process() - return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) - - -@app.route('/pitstops/delete/', methods=['GET', 'POST']) -@login_required -def delete_pit_stop_form(pid): - pitstop = Pitstop.query.filter(Pitstop.id == pid).first() - if pitstop is None: - return redirect(url_for('get_pit_stops')) - vehicle = Vehicle.query.filter(Vehicle.id == pitstop.vehicle_id).first() - if vehicle not in current_user.vehicles: - return redirect(url_for('get_pit_stops')) - - form = DeletePitStopForm() - if form.validate_on_submit(): - db.session.delete(pitstop) - db.session.commit() - tools.db_log_delete(pitstop) - return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) - - return render_template('deletePitstopForm.html', form=form, pitstop=pitstop ) - - -@app.route('/pitstops/edit/', methods=['GET', 'POST']) -@login_required -def edit_pit_stop_form(pid): - edit_pitstop = Pitstop.query.get(pid) - 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')) - - last_pitstop_pos = vehicle.pitstops.index(edit_pitstop) - 1 - if last_pitstop_pos > 0: - last_pitstop = vehicle.pitstops[last_pitstop_pos] - else: - last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) - - form = EditPitstopForm() - form.set_pitstop(last_pitstop) - - if form.validate_on_submit(): - edit_pitstop.costs = form.costs.data - edit_pitstop.date = form.date.data - edit_pitstop.amount = form.litres.data - edit_pitstop.odometer = form.odometer.data - db.session.commit() - tools.db_log_update(edit_pitstop) - return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) - - form.odometer.default = edit_pitstop.odometer - form.litres.default = edit_pitstop.amount - form.date.default = edit_pitstop.date - form.costs.default = edit_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)) - } - if edit_pitstop.costs is not None and edit_pitstop.costs > 0: - messages['costs'] = 'Costs must be higher than 0.01 €.' - return render_template('editPitStopForm.html', form=form, vehicle=vehicle, messages=messages) - - -def pitstop_service_key(x): - return x.odometer - - -@app.route('/pitstops', methods=['GET']) -@login_required -def get_pit_stops(): - user = { - 'vehicles': [] - } - for vehicle in current_user.vehicles: - data = [] - for pitstop in vehicle.pitstops: - data.append(pitstop) - for service in vehicle.services: - data.append(service) - v = { - 'id': vehicle.id, - 'name': vehicle.name, - 'data': sorted(data, key=pitstop_service_key) - } - user['vehicles'].append(v) - - return render_template('pitstops.html', user=user) - - -@app.route('/service/vehicle//create', methods=['GET', 'POST']) -@login_required -def create_service_for_vehicle(vid): - vehicle = Vehicle.query.get(vid) - if vehicle is None or vehicle not in current_user.vehicles: - return redirect(url_for('get_account_page')) - - form = CreateServiceForm() - last_pitstop = tools.get_latest_pitstop_for_vehicle(vid) - form.set_pitstop(last_pitstop) - form.same_odometer_allowed = True - - form.preinit_with_data() - - if form.validate_on_submit(): - new_service = Service(form.date.data, form.odometer.data, vid, form.costs.data, form.description.data) - db.session.add(new_service) - vehicle.services.append(new_service) - db.session.commit() - print(new_service) - return redirect(url_for('get_account_page')) - - form.process() - return render_template('createServiceForm.html', form=form, vehicle=vehicle, messages=[]) - - -@app.route('/service/delete/', methods=['GET', 'POST']) -@login_required -def delete_service_form(sid): - service = Service.query.filter(Service.id == sid).first() - if service is None: - return redirect(url_for('get_pit_stops')) - vehicle = Vehicle.query.filter(Vehicle.id == service.vehicle_id).first() - if vehicle not in current_user.vehicles: - return redirect(url_for('get_pit_stops')) - - form = DeleteServiceForm() - if form.validate_on_submit(): - db.session.delete(service) - db.session.commit() - tools.db_log_delete(service) - return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) - - return render_template('deleteServiceForm.html', form=form, service=service ) - - -@app.route('/service/edit/', methods=['GET', 'POST']) -@login_required -def edit_service_form(sid): - edit_service = Service.query.get(sid) - if edit_service is None: - return redirect(url_for('get_pit_stops')) - - vehicle = Vehicle.query.filter(Vehicle.id == edit_service.vehicle_id).first() - if vehicle not in current_user.vehicles: - return redirect(url_for('get_pit_stops')) - - # last_pitstop_pos = vehicle.pitstops.index(edit_service) - 1 - # if last_pitstop_pos > 0: - # last_pitstop = vehicle.pitstops[last_pitstop_pos] - # else: - # last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) - last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) - - form = EditServiceForm() - form.set_pitstop(last_pitstop) - - if form.validate_on_submit(): - edit_service.costs = form.costs.data - edit_service.date = form.date.data - edit_service.description = form.description.data - edit_service.odometer = form.odometer.data - db.session.commit() - tools.db_log_update(edit_service) - return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) - - form.odometer.default = edit_service.odometer - form.description.default = edit_service.description - form.date.default = edit_service.date - form.costs.default = edit_service.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)) - } - if edit_service.costs is not None and edit_service.costs > 0: - messages['costs'] = 'Costs must be higher than 0.01 €.' - return render_template('editServiceForm.html', form=form, vehicle=vehicle, messages=messages) - - -@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(): - 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']) -@login_required -def get_account_page(): - 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(tools.VehicleStats(vehicle)) - return render_template('statistics.html', data=stats) - - -@app.route('/account/delete', methods=['GET', 'POST']) -@login_required -def delete_account(): - form = DeleteAccountForm() - - if form.validate_on_submit(): - user_datastore.delete_user(current_user) - db.session.commit() - return redirect(url_for('index')) - - return render_template('deleteAccountForm.html', form=form) diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..f5a1dc4 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,5 @@ +from .account import * +from .admin import * +from .misc import * +from .pitstop import * +from .service import * diff --git a/app/routes/account.py b/app/routes/account.py new file mode 100644 index 0000000..ae4780a --- /dev/null +++ b/app/routes/account.py @@ -0,0 +1,131 @@ +from flask import url_for, redirect, render_template +from flask_security import login_required +from flask_security.core import current_user +from sqlalchemy.exc import IntegrityError + +from ..entities import Vehicle, Consumable +from ..forms import EditVehicleForm, DeleteVehicleForm, DeleteAccountForm +from ..tools import db_log_update, db_log_delete, db_log_add +from .. import app, db, user_datastore + + +@app.route('/account', methods=['GET']) +@login_required +def get_account_page(): + return render_template('account.html') + + +@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(): + vehicle.name = form.name.data + # 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() + 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, vehicle=vehicle) + + +@app.route('/account/vehicle/delete/', 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')) + + if len(current_user.vehicles) == 1: + return redirect(url_for('get_account_page')) + + form = DeleteVehicleForm() + + if form.validate_on_submit(): + db.session.delete(vehicle) + db.session.commit() + db_log_delete(vehicle) + return redirect(url_for('get_account_page')) + + return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle) + + +@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 + else: + form.consumables.default = [] + + if form.validate_on_submit(): + 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) + try: + db.session.commit() + 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('/account/delete', methods=['GET', 'POST']) +@login_required +def delete_account(): + form = DeleteAccountForm() + + if form.validate_on_submit(): + user_datastore.delete_user(current_user) + db.session.commit() + return redirect(url_for('index')) + + return render_template('deleteAccountForm.html', form=form) + diff --git a/app/routes/admin.py b/app/routes/admin.py new file mode 100644 index 0000000..a978daf --- /dev/null +++ b/app/routes/admin.py @@ -0,0 +1,96 @@ +from flask import url_for, redirect, render_template +from flask_security import login_required, roles_required +from sqlalchemy.exc import IntegrityError + +from ..entities import User, Consumable +from ..forms import CreateConsumableForm, DeletConsumableForm, EditConsumableForm +from ..tools import db_log_update, db_log_delete, db_log_add +from .. import app, db + + +@app.route('/admin', methods=['GET']) +@roles_required('admin') +def get_admin_page(): + 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() + 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() + 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() + 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) + diff --git a/app/routes/misc.py b/app/routes/misc.py new file mode 100644 index 0000000..1aff06a --- /dev/null +++ b/app/routes/misc.py @@ -0,0 +1,22 @@ +from flask import render_template +from flask_security import login_required +from flask_security.core import current_user + +from ..tools import VehicleStats +from .. import app + + +@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) + + +@app.route('/manual', methods=['GET']) +@login_required +def get_manual(): + return render_template('manual.html') + diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py new file mode 100644 index 0000000..e69a223 --- /dev/null +++ b/app/routes/pitstop.py @@ -0,0 +1,187 @@ +from flask import url_for, redirect, render_template, flash +from flask_security import login_required +from flask_security.core import current_user +from sqlalchemy.exc import IntegrityError +from datetime import date + +from ..entities import Vehicle, Consumable, Pitstop +from ..forms import SelectVehicleForm, SelectConsumableForm, CreatePitstopForm, EditPitstopForm, DeletePitStopForm +from ..tools import db_log_update, db_log_delete, db_log_add, get_latest_pitstop_for_vehicle, \ + get_latest_pitstop_for_vehicle_and_consumable, compute_lower_limits_for_new_pitstop +from .. import app, db + + +@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(): + return redirect(url_for('select_consumable_for_new_pitstop', vid=form.vehicle.data)) + + return render_template('selectVehicle.html', form=form) + + +@app.route('/pitstops/vehicle//consumable/select', methods=['GET', 'POST']) +@login_required +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.consumables) == 0: + flash('Please choose at least one consumable!', 'warning') + return redirect(url_for('edit_vehicle', vid=vid)) + + if len(vehicle.consumables) == 1: + return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id)) + + form = SelectConsumableForm() + form.consumable.choices = [(g.id, g.name) for g in vehicle.consumables] + + if form.validate_on_submit(): + 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 = get_latest_pitstop_for_vehicle(vid) + last_pitstop_consumable = 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(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) + try: + db.session.commit() + 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.process() + return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) + + +@app.route('/pitstops/delete/', methods=['GET', 'POST']) +@login_required +def delete_pit_stop_form(pid): + pitstop = Pitstop.query.filter(Pitstop.id == pid).first() + if pitstop is None: + return redirect(url_for('get_pit_stops')) + vehicle = Vehicle.query.filter(Vehicle.id == pitstop.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for('get_pit_stops')) + + form = DeletePitStopForm() + if form.validate_on_submit(): + db.session.delete(pitstop) + db.session.commit() + db_log_delete(pitstop) + return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) + + return render_template('deletePitstopForm.html', form=form, pitstop=pitstop ) + + +@app.route('/pitstops/edit/', methods=['GET', 'POST']) +@login_required +def edit_pit_stop_form(pid): + edit_pitstop = Pitstop.query.get(pid) + 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')) + + last_pitstop_pos = vehicle.pitstops.index(edit_pitstop) - 1 + if last_pitstop_pos > 0: + last_pitstop = vehicle.pitstops[last_pitstop_pos] + else: + last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) + + form = EditPitstopForm() + form.set_pitstop(last_pitstop) + + if form.validate_on_submit(): + edit_pitstop.costs = form.costs.data + edit_pitstop.date = form.date.data + edit_pitstop.amount = form.litres.data + edit_pitstop.odometer = form.odometer.data + db.session.commit() + db_log_update(edit_pitstop) + return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) + + form.odometer.default = edit_pitstop.odometer + form.litres.default = edit_pitstop.amount + form.date.default = edit_pitstop.date + form.costs.default = edit_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)) + } + if edit_pitstop.costs is not None and edit_pitstop.costs > 0: + messages['costs'] = 'Costs must be higher than 0.01 €.' + return render_template('editPitStopForm.html', form=form, vehicle=vehicle, messages=messages) + + +def pitstop_service_key(x): + return x.odometer + + +@app.route('/pitstops', methods=['GET']) +@login_required +def get_pit_stops(): + user = { + 'vehicles': [] + } + for vehicle in current_user.vehicles: + data = [] + for pitstop in vehicle.pitstops: + data.append(pitstop) + for service in vehicle.services: + data.append(service) + v = { + 'id': vehicle.id, + 'name': vehicle.name, + 'data': sorted(data, key=pitstop_service_key) + } + user['vehicles'].append(v) + + return render_template('pitstops.html', user=user) diff --git a/app/routes/service.py b/app/routes/service.py new file mode 100644 index 0000000..5272051 --- /dev/null +++ b/app/routes/service.py @@ -0,0 +1,98 @@ +from flask import url_for, redirect, render_template +from flask_security import login_required, current_user +from datetime import date + +from ..entities import Vehicle, Service, Pitstop +from ..forms import CreateServiceForm, DeleteServiceForm, EditServiceForm +from ..tools import db_log_update, db_log_delete, db_log_add, get_latest_pitstop_for_vehicle +from .. import app, db + + +@app.route('/service/vehicle//create', methods=['GET', 'POST']) +@login_required +def create_service_for_vehicle(vid): + vehicle = Vehicle.query.get(vid) + if vehicle is None or vehicle not in current_user.vehicles: + return redirect(url_for('get_account_page')) + + form = CreateServiceForm() + last_pitstop = get_latest_pitstop_for_vehicle(vid) + form.set_pitstop(last_pitstop) + form.same_odometer_allowed = True + + form.preinit_with_data() + + if form.validate_on_submit(): + new_service = Service(form.date.data, form.odometer.data, vid, form.costs.data, form.description.data) + db.session.add(new_service) + vehicle.services.append(new_service) + db.session.commit() + print(new_service) + return redirect(url_for('get_account_page')) + + form.process() + return render_template('createServiceForm.html', form=form, vehicle=vehicle, messages=[]) + + +@app.route('/service/delete/', methods=['GET', 'POST']) +@login_required +def delete_service_form(sid): + service = Service.query.filter(Service.id == sid).first() + if service is None: + return redirect(url_for('get_pit_stops')) + vehicle = Vehicle.query.filter(Vehicle.id == service.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for('get_pit_stops')) + + form = DeleteServiceForm() + if form.validate_on_submit(): + db.session.delete(service) + db.session.commit() + db_log_delete(service) + return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) + + return render_template('deleteServiceForm.html', form=form, service=service ) + + +@app.route('/service/edit/', methods=['GET', 'POST']) +@login_required +def edit_service_form(sid): + edit_service = Service.query.get(sid) + if edit_service is None: + return redirect(url_for('get_pit_stops')) + + vehicle = Vehicle.query.filter(Vehicle.id == edit_service.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for('get_pit_stops')) + + # last_pitstop_pos = vehicle.pitstops.index(edit_service) - 1 + # if last_pitstop_pos > 0: + # last_pitstop = vehicle.pitstops[last_pitstop_pos] + # else: + # last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) + last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0) + + form = EditServiceForm() + form.set_pitstop(last_pitstop) + + if form.validate_on_submit(): + edit_service.costs = form.costs.data + edit_service.date = form.date.data + edit_service.description = form.description.data + edit_service.odometer = form.odometer.data + db.session.commit() + db_log_update(edit_service) + return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) + + form.odometer.default = edit_service.odometer + form.description.default = edit_service.description + form.date.default = edit_service.date + form.costs.default = edit_service.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)) + } + if edit_service.costs is not None and edit_service.costs > 0: + messages['costs'] = 'Costs must be higher than 0.01 €.' + return render_template('editServiceForm.html', form=form, vehicle=vehicle, messages=messages)