diff --git a/app/__init__.py b/app/__init__.py index c2d0e09..0c226bb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,30 +1,14 @@ -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 \ - CreatePitstopForm, \ - EditVehicleForm, \ - DeleteVehicleForm, \ - SelectVehicleForm, \ - DeleteAccountForm, \ - DeletePitStopForm, \ - EditPitstopForm, \ - CreateConsumableForm, \ - EditConsumableForm, \ - DeletConsumableForm, \ - SelectConsumableForm +from .forms import * app = Flask(__name__) @@ -33,30 +17,25 @@ 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 - -# required to activate the filters -from .filters import * -from .tools import * - +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 * +from .routes import * + @user_registered.connect_via(app) -def user_registered_sighandler(app, user, confirm_token): +def user_registered_sighandler(application, user, confirm_token): """ Called after a user was created """ role = user_datastore.find_role('user') user_datastore.add_role_to_user(user, role) - if user.email == app.config['ADMIN_MAIL']: + if user.email == application.config['ADMIN_MAIL']: # if the user selected the preconfigured email for the admin account role = user_datastore.find_role('admin') user_datastore.add_role_to_user(user, role) @@ -114,381 +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) - - -@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(): - 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/entities.py b/app/entities.py index 5e06001..d138bbf 100644 --- a/app/entities.py +++ b/app/entities.py @@ -63,6 +63,9 @@ class Vehicle(db.Model): pitstops = db.relationship( 'Pitstop' ) + services = db.relationship( + 'Service' + ) consumables = db.relationship( 'Consumable', secondary=vehicles_consumables @@ -140,3 +143,23 @@ class Consumable(db.Model): def __repr__(self): return '' % (self.name, self.unit) + + +class Service(db.Model): + id = db.Column(db.Integer, primary_key=True) + date = db.Column(db.Date) + odometer = db.Column(db.Integer) + vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id')) + costs = db.Column(db.Numeric(10, 2), default=0) + description = db.Column(db.String(4096)) + + def __init__(self, date, odometer, vehicle_id, costs, description): + self.description = description + self.costs = costs + self.date = date + self.odometer = odometer + self.vehicle_id = vehicle_id + + def __repr__(self): + return '' % \ + (self.odometer, self.date, self.vehicle_id, self.costs, self.description) diff --git a/app/filters.py b/app/filters.py index 6aadde6..c346c66 100644 --- a/app/filters.py +++ b/app/filters.py @@ -1,5 +1,6 @@ from app import app import hashlib +import markdown @app.template_filter('none_filter') @@ -20,3 +21,8 @@ def md5_filter(value): @app.template_filter('str') def str_filter(value): return str(value) + + +@app.template_filter('markdown') +def md(value): + return markdown.markdown(value) diff --git a/app/forms.py b/app/forms.py deleted file mode 100644 index 5a51f8c..0000000 --- a/app/forms.py +++ /dev/null @@ -1,169 +0,0 @@ -from flask_wtf import Form -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(): - raise ValidationError('The new date must not be after %s' % date.today()) - - -def odometer_check(form, field): - """ - - :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) - - -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') - - -def costs_check(form, field): - if field.data is not None and field.data <= 0: - raise ValidationError('Costs must be above 0.01 €.') - - -def edit_costs_check(form, field): - """ - 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 €.') - - -class SelectVehicleForm(Form): - vehicle = SelectField('Vehicle', coerce=int) - 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]) - litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) - 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 = date.today() - 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!') - - -class DeleteVehicleForm(Form): - submit = SubmitField(label='Do it!') - - -class DeleteAccountForm(Form): - submit = SubmitField(label='Really delete my account!') - - -class DeletePitStopForm(Form): - submit = SubmitField(label='Really delete this pitstop!') - - -class EditPitstopForm(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]) - 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/forms/__init__.py b/app/forms/__init__.py new file mode 100644 index 0000000..e4f65bc --- /dev/null +++ b/app/forms/__init__.py @@ -0,0 +1,6 @@ +from .misc import * +from .pitstop import * +from .checks import * +from .consumable import * +from .service import * +from .vehicle import * diff --git a/app/forms/checks.py b/app/forms/checks.py new file mode 100644 index 0000000..ff8b1bb --- /dev/null +++ b/app/forms/checks.py @@ -0,0 +1,53 @@ +from wtforms.validators import ValidationError +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(): + raise ValidationError('The new date must not be after %s' % date.today()) + + +def odometer_check(form, field): + """ + + :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) + + +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') + + +def costs_check(form, field): + if field.data is not None and field.data <= 0: + raise ValidationError('Costs must be above 0.01 €.') + + +def edit_costs_check(form, field): + """ + 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 €.') + + diff --git a/app/forms/consumable.py b/app/forms/consumable.py new file mode 100644 index 0000000..a63b3e6 --- /dev/null +++ b/app/forms/consumable.py @@ -0,0 +1,24 @@ +from flask_wtf import FlaskForm +from wtforms import SelectField, StringField, SubmitField +from wtforms.validators import Length + + +class SelectConsumableForm(FlaskForm): + consumable = SelectField('Consumable', coerce=int) + submit = SubmitField(label='Do it!') + + +class CreateConsumableForm(FlaskForm): + name = StringField('Name', validators=[Length(1, 255)]) + unit = StringField('Unit', validators=[Length(1, 255)]) + submit = SubmitField(label='Do it!') + + +class EditConsumableForm(FlaskForm): + name = StringField('Name', validators=[Length(1, 255)]) + unit = StringField('Unit', validators=[Length(1, 255)]) + submit = SubmitField(label='Do it!') + + +class DeletConsumableForm(FlaskForm): + submit = SubmitField(label='Do it!') \ No newline at end of file diff --git a/app/forms/misc.py b/app/forms/misc.py new file mode 100644 index 0000000..7a4afd9 --- /dev/null +++ b/app/forms/misc.py @@ -0,0 +1,6 @@ +from flask_wtf import FlaskForm +from wtforms import SubmitField + + +class DeleteAccountForm(FlaskForm): + submit = SubmitField(label='Really delete my account!') diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py new file mode 100644 index 0000000..842eacb --- /dev/null +++ b/app/forms/pitstop.py @@ -0,0 +1,82 @@ +from flask_wtf import FlaskForm +from wtforms import DateField, IntegerField, DecimalField, SubmitField + +from .checks import * + + +class DeletePitStopForm(FlaskForm): + submit = SubmitField(label='Really delete this pitstop!') + + +class EditPitstopForm(FlaskForm): + date = DateField('Date of Pitstop', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) + 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 CreatePitstopForm(FlaskForm): + date = DateField('Date of Pitstop', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) + 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 = date.today() + 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 + + diff --git a/app/forms/service.py b/app/forms/service.py new file mode 100644 index 0000000..3e11c62 --- /dev/null +++ b/app/forms/service.py @@ -0,0 +1,68 @@ +from flask_wtf import FlaskForm +from wtforms import DateField, IntegerField, DecimalField, SubmitField, TextAreaField +from wtforms.validators import Length + +from .checks import * + + +class CreateServiceForm(FlaskForm): + date = DateField('Date of Service', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) + description = TextAreaField('Description', validators=[Length(1, 4096)]) + submit = SubmitField(label='Do it!') + last_pitstop = None + + def set_pitstop(self, last_pitstop): + self.last_pitstop = last_pitstop + + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + else: + self.date.default = date.today() + + if self.odometer.data: + self.odometer.default = self.odometer.data + else: + self.odometer.default = self.last_pitstop.odometer + + if self.costs.data: + self.costs.default = self.costs.data + else: + self.costs.default = 0 + + +class DeleteServiceForm(FlaskForm): + submit = SubmitField(label='Really delete this service!') + + +class EditServiceForm(FlaskForm): + date = DateField('Date of Service', validators=[date_check]) + odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) + description = TextAreaField('Description', validators=[Length(1, 4096)]) + submit = SubmitField(label='Do it!') + last_pitstop = None + same_odometer_allowed = True + + def set_pitstop(self, last_pitstop): + self.last_pitstop = last_pitstop + + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + else: + self.date.default = date.today() + + if self.odometer.data: + self.odometer.default = self.odometer.data + else: + self.odometer.default = self.last_pitstop.odometer + + if self.costs.data: + self.costs.default = self.costs.data + else: + self.costs.default = 0 + + diff --git a/app/forms/vehicle.py b/app/forms/vehicle.py new file mode 100644 index 0000000..4e42080 --- /dev/null +++ b/app/forms/vehicle.py @@ -0,0 +1,20 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, SelectField, SelectMultipleField +from wtforms.validators import Length + + +class SelectVehicleForm(FlaskForm): + vehicle = SelectField('Vehicle', coerce=int) + submit = SubmitField(label='Do it!') + + +class EditVehicleForm(FlaskForm): + name = StringField('Name', validators=[Length(1, 255)]) + consumables = SelectMultipleField('Consumables', coerce=int,validators=[]) + submit = SubmitField(label='Do it!') + + +class DeleteVehicleForm(FlaskForm): + submit = SubmitField(label='Do it!') + + 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..cb54f3b --- /dev/null +++ b/app/routes/pitstop.py @@ -0,0 +1,184 @@ +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, pitstop_service_key, \ + get_event_line_for_vehicle +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() + + data = get_event_line_for_vehicle(vehicle) + if len(data) > 0: + form.set_pitstop(Pitstop(data[-1].odometer, 0, data[-1].date, 0, cid)) + form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (data[-1].consumable.id != cid) + else: + form.set_pitstop(Pitstop(date(1970, 1, 1), 0, vid, 0, '')) + form.set_pitstop(Pitstop(0, 0, date(1970, 1, 1), 0, cid)) + form.same_odometer_allowed = True + + # 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) + + +@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) + data.sort(key=pitstop_service_key) + v = { + 'id': vehicle.id, + 'name': vehicle.name, + 'data': data + } + 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..4a5a9dd --- /dev/null +++ b/app/routes/service.py @@ -0,0 +1,120 @@ +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 +from ..forms import CreateServiceForm, DeleteServiceForm, EditServiceForm, SelectVehicleForm +from ..tools import db_log_update, db_log_delete, get_event_line_for_vehicle, 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() + + data = get_event_line_for_vehicle(vehicle) + if len(data) > 0: + form.set_pitstop(Service(data[-1].date, data[-1].odometer, vid, 0, '')) + form.same_odometer_allowed = type(data[-1]) != Service + else: + form.set_pitstop(Service(date(1970, 1, 1), 0, vid, 0, '')) + 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() + return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id))) + + 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')) + + form = EditServiceForm() + data = get_event_line_for_vehicle(vehicle) + data.reverse() + if len(data) > 0: + last_pitstop = Service(data[-1].date, data[-1].odometer, vehicle.id, 0, '') + else: + last_pitstop = Service(date(1970, 1, 1), 0, vehicle.id, 0, '') + form.set_pitstop(last_pitstop) + form.same_odometer_allowed = True + + 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) + + +@app.route('/service/vehicle/select', methods=['GET', 'POST']) +@login_required +def select_vehicle_for_new_service(): + if len(current_user.vehicles) == 1: + return redirect(url_for('create_service_for_vehicle', 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('create_service_for_vehicle', vid=form.vehicle.data)) + + return render_template('selectVehicle.html', form=form) + + diff --git a/app/static/main.css b/app/static/main.css index 73b9e3b..d863e1a 100644 --- a/app/static/main.css +++ b/app/static/main.css @@ -1,3 +1,12 @@ +.markdown > h1 { + font-size: 14px; + font-weight: bold; +} + +.markdown { + text-align: left +} + body { padding-top: 50px; } diff --git a/app/templates/account.html b/app/templates/account.html index e408636..004a005 100644 --- a/app/templates/account.html +++ b/app/templates/account.html @@ -37,9 +37,16 @@ {{ vehicle.pitstops | length }} pitstops
+ {{ vehicle.services | length }} special expenses
{{ vehicle.consumables | length }} consumables + + add service + + + add pitstop + edit diff --git a/app/templates/createServiceForm.html b/app/templates/createServiceForm.html new file mode 100644 index 0000000..68758f4 --- /dev/null +++ b/app/templates/createServiceForm.html @@ -0,0 +1,30 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

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

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.date) }} + + {{messages['date']}} + + {{ render_field_with_errors(form.odometer) }} + + {{messages['odometer']}} + + {{ render_field_with_errors(form.costs) }} + + {{messages['costs']}} + + {{ render_field_with_errors(form.description) }} + {{ render_field_with_errors(form.submit) }} +
+
+
+
+
+{% endblock %} diff --git a/app/templates/deletePitstopForm.html b/app/templates/deletePitstopForm.html index 871ec67..635ff68 100644 --- a/app/templates/deletePitstopForm.html +++ b/app/templates/deletePitstopForm.html @@ -6,7 +6,7 @@

Delete pitstop?

- +
diff --git a/app/templates/deleteServiceForm.html b/app/templates/deleteServiceForm.html new file mode 100644 index 0000000..9448197 --- /dev/null +++ b/app/templates/deleteServiceForm.html @@ -0,0 +1,42 @@ +{% extends 'layout.html' %} + +{% block body %} +
+
+
+
+

Delete service?

+
Date of Pitstop {{ pitstop.date }}
+ + + + + + + + + + + + + + + + +
Date of Pitstop{{ service.date }}
Odometer{{ service.odometer }} km
Description{{ service.description | markdown | safe}}
Costs (overall) + {% if service.costs %} + {{service.costs}} + {% else %} + -- + {% endif %} + € +
+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.submit) }} +
+
+
+ +
+{% endblock %} diff --git a/app/templates/editServiceForm.html b/app/templates/editServiceForm.html new file mode 100644 index 0000000..c1bfe62 --- /dev/null +++ b/app/templates/editServiceForm.html @@ -0,0 +1,29 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

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

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.date) }} + + {{messages['date']}} + + {{ render_field_with_errors(form.odometer) }} + + {{messages['odometer']}} + + {{ render_field_with_errors(form.description) }} + {{ render_field_with_errors(form.costs) }} + + {{messages['costs']}} + + {{ render_field_with_errors(form.submit) }} +
+
+
+
+{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index 91314bf..3420b4d 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -1,6 +1,7 @@ {% macro navigation() -%} {% if current_user.email %}
  • Create Pitstop
  • +
  • Create Service
  • Statistics
  • Account
  • {% if current_user.has_role('admin') %} @@ -56,6 +57,8 @@ {% elif field.type == 'DecimalField' %} + {% elif field.type == 'TextAreaField' %} + {% else %} {{ field(**kwargs)|safe }} {% endif %} diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html index 9a4cec5..0d177e1 100644 --- a/app/templates/pitstops.html +++ b/app/templates/pitstops.html @@ -1,9 +1,95 @@ - {% extends "layout.html" %} +{% extends "layout.html" %} + +{% macro pitstop(field, vindex, loop) -%} +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + +
    Date{{field.date}}
    Odometer{{field.odometer}} km
    {{ field.consumable.name }}{{field.amount}} {{ field.consumable.unit }}
    Costs + {% if field.costs %} + {{field.costs}} € + {% else %} + -- € + {% endif %} +
    + {% if loop.first %} + + edit + + + delete + + {% endif %} +
    +
    +{%- endmacro %} + +{% macro service(field, vindex, loop) -%} +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + +
    Date{{field.date}}
    Odometer{{field.odometer}} km
    Description{{field.description | markdown | safe}}
    Costs + {% if field.costs %} + {{field.costs}} € + {% else %} + -- € + {% endif %} +
    + {% if loop.first %} + + edit + + + delete + + {% endif %} +
    +
    +{%- endmacro %} + + {% block body %}
    - {% for vehicle in current_user.vehicles %} + {% for vehicle in user.vehicles %} {% set vehicleloop = loop %}

    {{vehicle.name}}

    - {% if vehicle.pitstops %} - {% for pitstop in vehicle.pitstops|reverse %} -
    + {% if vehicle.data %} + {% for data in vehicle.data|reverse %} + {% if 'Pitstop' in data.__class__.__name__ %} + {{ pitstop(data, vehicleloop.index, loop) }} + {% endif %} + {% if 'Service' in data.__class__.__name__ %} + {{ service(data, vehicleloop.index, loop) }} + {% endif %} {% endfor %} {% else %}