diff --git a/app/rollerverbrauch/__init__.py b/app/rollerverbrauch/__init__.py index c79ce53..070e3de 100644 --- a/app/rollerverbrauch/__init__.py +++ b/app/rollerverbrauch/__init__.py @@ -102,7 +102,7 @@ def index(): vehicle_count = len(vehicles) pitstop_count = len(Pitstop.query.all()) data = { - 'users':user_count, + 'users': user_count, 'vehicles': vehicle_count, 'pitstops': pitstop_count, 'litres': litres, @@ -256,36 +256,42 @@ def create_pit_stop_form(vid, cid): if consumable not in vehicle.consumables: return redirect(url_for('select_consumable_for_new_pitstop', vid=vid)) - # the last pitstop is required to be able to check the monotonicy of date and odometer - - if len(vehicle.pitstops) > 0: - last_pitstop = vehicle.pitstops[-1] - else: - last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, None) - form = CreatePitstopForm() - form.set_pitstop(last_pitstop) - form.litres.label = '%s (%s)' % (consumable.name, consumable.unit) + # the last pitstop is required to be able to check the monotonicy of date and odometer + last_pitstop = tools.get_latest_pitstop_for_vehicle(vid) + last_pitstop_consumable = tools.get_latest_pitstop_for_vehicle_and_consumable(vid, cid) + + # we can enter the same odometer if the pitstops are not equal + form.same_odometer_allowed = (last_pitstop != last_pitstop_consumable) + + # set the lower limits for odometer andd date and the values for amount and costs of the last stop + form.set_pitstop(tools.compute_lower_limits_for_new_pitstop(last_pitstop, last_pitstop_consumable, cid)) + + # set the label of the litres field to make the user comfortable + form.set_consumable(consumable) + + # preinitialize the defaults with potentially existing values from a try before + form.preinit_with_data() + + # + # Validate should accept same odometer on different consumables + # if form.validate_on_submit(): new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data, form.costs.data, cid) db.session.add(new_stop) vehicle.pitstops.append(new_stop) - db.session.commit() - tools.db_log_add(new_stop) + try: + db.session.commit() + tools.db_log_add(new_stop) + except IntegrityError: + db.session.rollback() + form.odometer.errors.append('Pitstop already present for %s at odometer %s km!' % (consumable.name, form.odometer.data)) + return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) return redirect(url_for('get_pit_stops', _anchor= 'v' + str(vehicle.id))) - form.odometer.default = last_pitstop.odometer -# form.litres.default = last_pitstop.litres - form.date.default = date.today() -# form.costs.default = last_pitstop.costs form.process() - messages = { - 'date': 'Date must be between %s and %s (including).' % (str(last_pitstop.date), str(date.today())), - 'odometer': 'Odometer must be greater than %s km.' % (str(last_pitstop.odometer)), - 'costs': 'Costs must be higher than 0.01 €.' - } - return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=messages) + return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) @app.route('/pitstops/delete/', methods=['GET', 'POST']) @@ -311,9 +317,10 @@ def delete_pit_stop_form(pid): @app.route('/pitstops/edit/', methods=['GET', 'POST']) @login_required def edit_pit_stop_form(pid): - edit_pitstop = Pitstop.query.filter(Pitstop.id == pid).first() + edit_pitstop = Pitstop.query.get(pid).first() if edit_pitstop is None: return redirect(url_for('get_pit_stops')) + vehicle = Vehicle.query.filter(Vehicle.id == edit_pitstop.vehicle_id).first() if vehicle not in current_user.vehicles: return redirect(url_for('get_pit_stops')) diff --git a/app/rollerverbrauch/entities.py b/app/rollerverbrauch/entities.py index 0a9c06e..b59a917 100644 --- a/app/rollerverbrauch/entities.py +++ b/app/rollerverbrauch/entities.py @@ -11,6 +11,9 @@ vehicles_consumables = db.Table('vehicles_consumables', class Role(db.Model, RoleMixin): + """ + Entity to handle different roles for users: Typically user and admin exist + """ id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(80), unique=True) description = db.Column(db.String(255)) @@ -23,11 +26,15 @@ class Role(db.Model, RoleMixin): class User(db.Model, UserMixin): + """ + Entity to represent a user including login data and links to roles and vehicles. + """ id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(255), unique=True) password = db.Column(db.String(255)) active = db.Column(db.Boolean()) confirmed_at = db.Column(db.DateTime()) + vehicles = db.relationship( 'Vehicle' ) @@ -42,6 +49,14 @@ class User(db.Model, UserMixin): class Vehicle(db.Model): + """ + Entity to represent a vehicle. + Attributes: + * name of the vehilce + * the id of the owner + * list of pitstops + * list of possible consumables + """ id = db.Column(db.Integer, primary_key=True) owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) name = db.Column(db.String(255)) @@ -52,7 +67,10 @@ class Vehicle(db.Model): 'Consumable', secondary=vehicles_consumables ) - __table_args__ = (db.UniqueConstraint('owner_id', 'name', name='_owner_name_uniq'),) + # allow vehicle names to be duplicated between different owners but must still be uniq for each owner + __table_args__ = (db.UniqueConstraint('owner_id', + 'name', + name='_owner_name_uniq'),) def __init__(self, name): self.name = name @@ -62,31 +80,55 @@ class Vehicle(db.Model): class Pitstop(db.Model): + """ + Entity to represent a pitstop for a single consumable. + Attributes: + * the date of the pitstop + * the odometer of the pitstop + * the id of the fuelled consumable + * amount of consumable used + * the costs of the consumable + * the id of the vehicle that was refuelled + """ id = db.Column(db.Integer, primary_key=True) date = db.Column(db.Date) odometer = db.Column(db.Integer) consumable_id = db.Column(db.Integer, db.ForeignKey('consumable.id')) - consumable = db.relationship('Consumable') - litres = db.Column(db.Numeric(5, 2)) + amount = db.Column(db.Numeric(5, 2)) costs = db.Column(db.Numeric(5, 2), default=0) vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id')) + # short cut to access the fuelled consumable of the pitstop + consumable = db.relationship('Consumable') + # this uniqueness constraint makes sure that for each consumable and each vehicle only one pitstop exists at the + # same odometer + __table_args__ = (db.UniqueConstraint('odometer', + 'consumable_id', + 'vehicle_id', + name='_odometer_consumable_vehicle_uniq'),) - def __init__(self, odometer, litres, date, costs, consumable_id): + def __init__(self, odometer, amount, date, costs, consumable_id): self.odometer = odometer - self.litres = litres + self.amount = amount self.date = date self.costs = costs self.consumable_id = consumable_id def __repr__(self): - return '' % \ - (self.odometer, self.litres, self.date, self.vehicle_id, self.consumable_id) + return '' % \ + (self.odometer, self.amount, self.date, self.vehicle_id, self.consumable_id) class Consumable(db.Model): + """ + Entity to represent a material that be consumed by a vehilce. + Attributes: + * name (must be globally unique) + * unit + """ id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), unique=True) unit = db.Column(db.String(255)) + vehicles = db.relationship( 'Vehicle', secondary=vehicles_consumables diff --git a/app/rollerverbrauch/forms.py b/app/rollerverbrauch/forms.py index 4bab1b5..f884643 100644 --- a/app/rollerverbrauch/forms.py +++ b/app/rollerverbrauch/forms.py @@ -5,6 +5,13 @@ from datetime import date def date_check(form, field): + """ + Checks that the date of the pitstop is not before the date of the latest pitstop and not after today. + + :param form: the form where the field is in + :param field: the field to check + :return: Nothing or a ValidationError if the limits are not kept + """ if field.data < form.last_pitstop.date: raise ValidationError('The new date must not be before %s' % form.last_pitstop.date) if field.data > date.today(): @@ -12,7 +19,15 @@ def date_check(form, field): def odometer_check(form, field): - if field.data <= form.last_pitstop.odometer: + """ + + :param form: + :param field: + :return: + """ + if not form.same_odometer_allowed and field.data <= form.last_pitstop.odometer: + raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer) + if form.same_odometer_allowed and field.data < form.last_pitstop.odometer: raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer) @@ -27,6 +42,12 @@ def costs_check(form, field): 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 €.') @@ -49,10 +70,44 @@ class CreatePitstopForm(Form): costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) submit = SubmitField(label='Do it!') last_pitstop = None + same_odometer_allowed = True def set_pitstop(self, last_pitstop): self.last_pitstop = last_pitstop + def set_consumable(self, consumable): + self.litres.label = '%s (%s)' % (consumable.name, consumable.unit) + + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + else: + self.date.default = self.last_pitstop.date + if self.odometer.data: + self.odometer.default = self.odometer.data + else: + self.odometer.default = self.last_pitstop.odometer + if self.litres.data: + self.litres.default = self.litres.data + else: + self.litres.default = self.last_pitstop.amount + if self.costs.data: + self.costs.default = self.costs.data + else: + self.costs.default = self.last_pitstop.costs + + def get_hint_messages(self): + if self.same_odometer_allowed: + or_equal = ' or equal to' + else: + or_equal = '' + messages = { + 'date': 'Date must be between %s and %s (including).' % (str(self.last_pitstop.date), str(date.today())), + 'odometer': 'Odometer must be greater than%s %s km.' % (or_equal, str(self.last_pitstop.odometer)), + 'costs': 'Costs must be higher than 0.01 €.' + } + return messages + class EditVehicleForm(Form): name = StringField('Name', validators=[Length(1, 255)]) @@ -79,10 +134,24 @@ class EditPitstopForm(Form): costs = DecimalField('Costs (€, overall)', places=2, validators=[edit_costs_check]) submit = SubmitField(label='Update it!') last_pitstop = None + same_odometer_allowed = True def set_pitstop(self, last_pitstop): self.last_pitstop = last_pitstop + def set_consumable(self, consumable): + self.litres.label = '%s (%s)' % (consumable.name, consumable.unit) + + def preinit_with_data(self): + if self.date.data: + self.date.default = self.date.data + if self.odometer.data: + self.odometer.default = self.odometer.data + if self.litres.data: + self.litres.default = self.litres.data + if self.costs.data: + self.costs.default = self.costs.data + class CreateConsumableForm(Form): name = StringField('Name', validators=[Length(1, 255)]) diff --git a/app/rollerverbrauch/templates/createPitStopForm.html b/app/rollerverbrauch/templates/createPitStopForm.html index feebbec..a6941e3 100644 --- a/app/rollerverbrauch/templates/createPitStopForm.html +++ b/app/rollerverbrauch/templates/createPitStopForm.html @@ -26,4 +26,5 @@ +
{% endblock %} diff --git a/app/rollerverbrauch/templates/pitstops.html b/app/rollerverbrauch/templates/pitstops.html index 7a3e773..88ca8a4 100644 --- a/app/rollerverbrauch/templates/pitstops.html +++ b/app/rollerverbrauch/templates/pitstops.html @@ -1,6 +1,7 @@ {% extends "layout.html" %} {% block body %} -
+
+
+ {% endfor %} {% else %}
+