From 5a961d096b7b1d6b3289e0955eac83641a1bbee4 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Fri, 10 Aug 2018 20:15:46 +0200 Subject: [PATCH 01/19] add tooltips to buttons for filling stations --- app/templates/account.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/templates/account.html b/app/templates/account.html index cd47fc3..6549bf6 100644 --- a/app/templates/account.html +++ b/app/templates/account.html @@ -71,9 +71,9 @@
-
-- 2.30.2 From 879669b4f78d5866e326f8ac9b5ff11a223540fd Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 13:05:51 +0200 Subject: [PATCH 02/19] Updates os version to debian 9 Changes to debian9_python3 image because of outdated setup tools Fixed a typo as well --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 20f3c37..d6ff606 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian8_python3 +FROM debian9_python3 COPY requirements.txt /requirements.txt RUN pip3 install -r /requirements.txt; \ @@ -9,6 +9,6 @@ ADD main.py /main.py ADD config.py /config.py VOLUME ["/data"] -VOLUME ["/app/config] +VOLUME ["/app/config"] EXPOSE 5000 ENTRYPOINT python3 /main.py -- 2.30.2 From c42a667ca445a6c2060fea861d0d13cbbd324f00 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 13:15:33 +0200 Subject: [PATCH 03/19] Introduces ordering for pitstops Pitstops are now ordered by ascending odometer value instead of their id --- app/entities.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/entities.py b/app/entities.py index b2f8006..8a3df07 100644 --- a/app/entities.py +++ b/app/entities.py @@ -73,7 +73,8 @@ class Vehicle(db.Model): owner_id = db.Column(db.Integer, db.ForeignKey('user.id')) name = db.Column(db.String(255)) pitstops = db.relationship( - 'Pitstop' + 'Pitstop', + order_by="asc(Pitstop.odometer)" ) services = db.relationship( 'Service' -- 2.30.2 From 1f2e33bec4058c04b599d8330d80d87625cbfb91 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 14:11:09 +0200 Subject: [PATCH 04/19] Adds check to verify the proper order of pitstops Pitstops must keep the order of date and odometer. That means if a pitstop's date is between two other pitstops' date than the odometer values must be as well. --- app/forms/checks.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/forms/checks.py b/app/forms/checks.py index ff8b1bb..fb44931 100644 --- a/app/forms/checks.py +++ b/app/forms/checks.py @@ -2,6 +2,31 @@ from wtforms.validators import ValidationError from datetime import date +def odometer_date_check(form, field): + """ + Checks that the entered date and odometer of the pitstop is conformant to the existing pitstops. That means, if a + pitstops date is between to other pitstops, the odometer should be as well. + :param form: + :param field: + :return: + """ + odometer = form.odometer.data + date = form.date.data + pitstops = form.pitstops + + if date < pitstops[0].date and odometer >= pitstops[0].odometer: + raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) + + for index in range(0, len(pitstops)-1): + if pitstops[index].date <= date < pitstops[index + 1].date: + if odometer <= pitstops[index].odometer or odometer >= pitstops[index+1].odometer: + raise ValidationError('The new odometer value must be greater than %i km and less than %i km' + % (pitstops[index].odometer,pitstops[index+1].odometer)) + + if date >= pitstops[-1].date and odometer <= pitstops[-1].odometer: + raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) + + 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. -- 2.30.2 From 25705562ad64a7db0e1cb8efe1cdca4da13a7f08 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 14:13:03 +0200 Subject: [PATCH 05/19] Displays informative messages on litre amount --- app/templates/createPitStopForm.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/templates/createPitStopForm.html b/app/templates/createPitStopForm.html index a6941e3..b8e42f0 100644 --- a/app/templates/createPitStopForm.html +++ b/app/templates/createPitStopForm.html @@ -17,6 +17,9 @@ {{messages['odometer']}} {{ render_field_with_errors(form.litres) }} + + {{messages['litres']}} + {{ render_field_with_errors(form.costs) }} {{messages['costs']}} -- 2.30.2 From 13090d273550f42955c56151885a8a799c23dcd9 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 14:53:08 +0200 Subject: [PATCH 06/19] Updates create pitstop form The form now uses the new odometer_date_check to allow entering pitstops that are before the currently last pitstop. --- app/forms/pitstop.py | 23 +++++++++-------------- app/routes/pitstop.py | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py index 842eacb..7b27c21 100644 --- a/app/forms/pitstop.py +++ b/app/forms/pitstop.py @@ -35,16 +35,16 @@ class EditPitstopForm(FlaskForm): class CreatePitstopForm(FlaskForm): - date = DateField('Date of Pitstop', validators=[date_check]) - odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + date = DateField('Date of Pitstop') + odometer = IntegerField('Odometer (km)', validators=[odometer_date_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 + pitstops = [] - def set_pitstop(self, last_pitstop): - self.last_pitstop = last_pitstop + def set_pitstops(self, pitstops): + self.pitstops = pitstops def set_consumable(self, consumable): self.litres.label = '%s (%s)' % (consumable.name, consumable.unit) @@ -57,24 +57,19 @@ class CreatePitstopForm(FlaskForm): if self.odometer.data: self.odometer.default = self.odometer.data else: - self.odometer.default = self.last_pitstop.odometer + self.odometer.default = self.pitstops[-1].odometer if self.litres.data: self.litres.default = self.litres.data else: - self.litres.default = self.last_pitstop.amount + self.litres.default = self.pitstops[-1].amount if self.costs.data: self.costs.default = self.costs.data else: - self.costs.default = self.last_pitstop.costs + self.costs.default = self.pitstops[-1].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)), + 'litres': 'Litres must be higher than 0.01 L.', 'costs': 'Costs must be higher than 0.01 €.' } return messages diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 40b739b..c0e6c81 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -64,7 +64,7 @@ def create_pit_stop_form(vid, cid): 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.set_pitstops(vehicle.pitstops) 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, '')) -- 2.30.2 From 3a2b25d3bd29cb5ddc20abc942f00eeb5a794f99 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 18:57:33 +0200 Subject: [PATCH 07/19] fixes issue with first pitstop for a vehicle --- app/routes/pitstop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index c0e6c81..7861a06 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -67,8 +67,7 @@ def create_pit_stop_form(vid, cid): form.set_pitstops(vehicle.pitstops) 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.set_pitstops([Pitstop(date(1970, 1, 1), 0, vid, 0, ''),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 -- 2.30.2 From 889a3e6476d657fb5a21e35e7931ccda83791f68 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 18:58:25 +0200 Subject: [PATCH 08/19] changes dev config to store sqlite file in data volume --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index a2d6bb9..9166ff5 100644 --- a/config.py +++ b/config.py @@ -30,7 +30,7 @@ class Config: class DevelopmentConfig(Config): SECURITY_SEND_REGISTER_EMAIL = False DEBUG = True - SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.sqlite') + SQLALCHEMY_DATABASE_URI = 'sqlite:////data/data.sqlite' class TestingConfig(Config): -- 2.30.2 From b3cb88e870db01c30902a4c50a6391a6cca45b87 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 11 Aug 2018 19:38:51 +0200 Subject: [PATCH 09/19] fix issue with creating the first ever pitstop for vehicle --- app/forms/checks.py | 21 ++++++++++++--------- app/forms/pitstop.py | 15 ++++++++++++--- app/routes/pitstop.py | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/forms/checks.py b/app/forms/checks.py index fb44931..f997c4d 100644 --- a/app/forms/checks.py +++ b/app/forms/checks.py @@ -14,17 +14,20 @@ def odometer_date_check(form, field): date = form.date.data pitstops = form.pitstops - if date < pitstops[0].date and odometer >= pitstops[0].odometer: - raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) + if len(pitstops) > 0: + if date < pitstops[0].date and odometer >= pitstops[0].odometer: + raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) - for index in range(0, len(pitstops)-1): - if pitstops[index].date <= date < pitstops[index + 1].date: - if odometer <= pitstops[index].odometer or odometer >= pitstops[index+1].odometer: - raise ValidationError('The new odometer value must be greater than %i km and less than %i km' - % (pitstops[index].odometer,pitstops[index+1].odometer)) + if date >= pitstops[-1].date and odometer <= pitstops[-1].odometer: + raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) + + if len(pitstops) > 1: + for index in range(0, len(pitstops)-1): + if pitstops[index].date <= date < pitstops[index + 1].date: + if odometer <= pitstops[index].odometer or odometer >= pitstops[index+1].odometer: + raise ValidationError('The new odometer value must be greater than %i km and less than %i km' + % (pitstops[index].odometer,pitstops[index+1].odometer)) - if date >= pitstops[-1].date and odometer <= pitstops[-1].odometer: - raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) def date_check(form, field): diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py index 7b27c21..27ff4da 100644 --- a/app/forms/pitstop.py +++ b/app/forms/pitstop.py @@ -54,18 +54,27 @@ class CreatePitstopForm(FlaskForm): self.date.default = self.date.data else: self.date.default = date.today() + if self.odometer.data: self.odometer.default = self.odometer.data - else: + elif len(self.pitstops) > 0: self.odometer.default = self.pitstops[-1].odometer + else: + self.odometer.default = 0 + if self.litres.data: self.litres.default = self.litres.data - else: + elif len(self.pitstops) > 0: self.litres.default = self.pitstops[-1].amount + else: + self.litres.default = 0 + if self.costs.data: self.costs.default = self.costs.data - else: + elif len(self.pitstops) > 0: self.costs.default = self.pitstops[-1].costs + else: + self.costs.default = 0 def get_hint_messages(self): messages = { diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 7861a06..9692496 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -67,7 +67,7 @@ def create_pit_stop_form(vid, cid): form.set_pitstops(vehicle.pitstops) form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (data[-1].consumable.id != cid) else: - form.set_pitstops([Pitstop(date(1970, 1, 1), 0, vid, 0, ''),Pitstop(0, 0, date(1970, 1, 1), 0, cid)]) + form.set_pitstops([]) form.same_odometer_allowed = True # set the label of the litres field to make the user comfortable -- 2.30.2 From bf4bb607d53d7c81ae41bef7213f66ca9a63ef23 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Wed, 15 Aug 2018 21:39:04 +0200 Subject: [PATCH 10/19] Introduces ordering for services services are now ordered by ascending odometer value instead of their id --- app/entities.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/entities.py b/app/entities.py index 8a3df07..d21fc0b 100644 --- a/app/entities.py +++ b/app/entities.py @@ -77,7 +77,8 @@ class Vehicle(db.Model): order_by="asc(Pitstop.odometer)" ) services = db.relationship( - 'Service' + 'Service', + order_by = "asc(Service.odometer)" ) consumables = db.relationship( 'Consumable', -- 2.30.2 From 6397d050aefb1860c8880c7bc1938d20ea0b7626 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Wed, 15 Aug 2018 21:45:22 +0200 Subject: [PATCH 11/19] services and pitstops each now respect their data on creation On creation, the tupels of date and odometer are checked that the service or pitstop does not violate the ordering in. --- app/forms/pitstop.py | 2 +- app/forms/service.py | 14 ++++++++------ app/routes/pitstop.py | 2 +- app/routes/service.py | 4 ++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py index 27ff4da..2df0ae1 100644 --- a/app/forms/pitstop.py +++ b/app/forms/pitstop.py @@ -64,7 +64,7 @@ class CreatePitstopForm(FlaskForm): if self.litres.data: self.litres.default = self.litres.data - elif len(self.pitstops) > 0: + elif len(self.pitstops) > 0 and 'amount' in self.pitstops[-1].__dict__: self.litres.default = self.pitstops[-1].amount else: self.litres.default = 0 diff --git a/app/forms/service.py b/app/forms/service.py index 3e11c62..76ef794 100644 --- a/app/forms/service.py +++ b/app/forms/service.py @@ -6,15 +6,15 @@ from .checks import * class CreateServiceForm(FlaskForm): - date = DateField('Date of Service', validators=[date_check]) - odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + date = DateField('Date of Pitstop') + odometer = IntegerField('Odometer (km)', validators=[odometer_date_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 + pitstops = [] - def set_pitstop(self, last_pitstop): - self.last_pitstop = last_pitstop + def set_pitstops(self, pitstops): + self.pitstops = pitstops def preinit_with_data(self): if self.date.data: @@ -24,8 +24,10 @@ class CreateServiceForm(FlaskForm): if self.odometer.data: self.odometer.default = self.odometer.data + elif len(self.pitstops) > 0: + self.odometer.default = self.pitstops[-1].odometer else: - self.odometer.default = self.last_pitstop.odometer + self.odometer.default = 0 if self.costs.data: self.costs.default = self.costs.data diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 9692496..75f4e5c 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -64,7 +64,7 @@ def create_pit_stop_form(vid, cid): data = get_event_line_for_vehicle(vehicle) if len(data) > 0: - form.set_pitstops(vehicle.pitstops) + form.set_pitstops(data) form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (data[-1].consumable.id != cid) else: form.set_pitstops([]) diff --git a/app/routes/service.py b/app/routes/service.py index 4a5a9dd..6c73520 100644 --- a/app/routes/service.py +++ b/app/routes/service.py @@ -19,10 +19,10 @@ def create_service_for_vehicle(vid): data = get_event_line_for_vehicle(vehicle) if len(data) > 0: - form.set_pitstop(Service(data[-1].date, data[-1].odometer, vid, 0, '')) + form.set_pitstops(data) form.same_odometer_allowed = type(data[-1]) != Service else: - form.set_pitstop(Service(date(1970, 1, 1), 0, vid, 0, '')) + form.set_pitstops([]) form.same_odometer_allowed = True form.preinit_with_data() -- 2.30.2 From 346723ae2dc2d0a90af65da8dd5f09a4157b56f1 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Wed, 15 Aug 2018 21:46:31 +0200 Subject: [PATCH 12/19] Makes each service/pitstop deletable adds a delete button to each service/pitstop --- app/templates/pitstops.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html index 0d177e1..4767128 100644 --- a/app/templates/pitstops.html +++ b/app/templates/pitstops.html @@ -34,10 +34,10 @@ edit + {% endif %} delete - {% endif %}
{%- endmacro %} @@ -76,10 +76,10 @@ edit + {% endif %} delete - {% endif %} {%- endmacro %} -- 2.30.2 From ae6dfa8ae6431672c6e097d9f97cf0ab64ebf8be Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Thu, 16 Aug 2018 06:58:11 +0200 Subject: [PATCH 13/19] Fix bug with description on services The service description was lost on any input error in the forms for create and edit. --- app/forms/service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/forms/service.py b/app/forms/service.py index 76ef794..bc19add 100644 --- a/app/forms/service.py +++ b/app/forms/service.py @@ -34,6 +34,9 @@ class CreateServiceForm(FlaskForm): else: self.costs.default = 0 + if self.description.data: + self.description.default = self.description.data + class DeleteServiceForm(FlaskForm): submit = SubmitField(label='Really delete this service!') @@ -67,4 +70,6 @@ class EditServiceForm(FlaskForm): else: self.costs.default = 0 + if self.description.data: + self.description.default = self.description.data -- 2.30.2 From 1d767d65a6988ab0dcd4f7d0944e4885391d47a8 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Thu, 16 Aug 2018 18:33:09 +0200 Subject: [PATCH 14/19] all pit stops can now be edited and deleted not only the last pit stop can be edited and deleted. --- app/forms/checks.py | 31 +++++++++++++++++++++++++++++-- app/forms/pitstop.py | 16 +++++++++++++--- app/routes/pitstop.py | 21 +++++++++------------ app/templates/pitstops.html | 2 +- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/app/forms/checks.py b/app/forms/checks.py index f997c4d..a7b827f 100644 --- a/app/forms/checks.py +++ b/app/forms/checks.py @@ -4,8 +4,8 @@ from datetime import date def odometer_date_check(form, field): """ - Checks that the entered date and odometer of the pitstop is conformant to the existing pitstops. That means, if a - pitstops date is between to other pitstops, the odometer should be as well. + Checks that the entered date and odometer of the pit stop is conformant to the existing pit stops. That means, if a + pitstops date is between two other pit stops, the odometer should be as well. :param form: :param field: :return: @@ -29,6 +29,33 @@ def odometer_date_check(form, field): % (pitstops[index].odometer,pitstops[index+1].odometer)) +def edit_odometer_date_check(form, field): + """ + This makes exactly the same checks as 'odometer_date_check' but the odometers may be the same (to change only amount + and price). + + :param form: + :param field: + :return: + """ + odometer = form.odometer.data + date = form.date.data + pitstops = form.pitstops + + if len(pitstops) > 0: + if date < pitstops[0].date and odometer > pitstops[0].odometer: + raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) + + if date >= pitstops[-1].date and odometer < pitstops[-1].odometer: + raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) + + if len(pitstops) > 1: + for index in range(0, len(pitstops)-1): + if pitstops[index].date <= date < pitstops[index + 1].date: + if odometer < pitstops[index].odometer or odometer > pitstops[index+1].odometer: + raise ValidationError('The new odometer value must be greater than %i km and less than %i km' + % (pitstops[index].odometer,pitstops[index+1].odometer)) + def date_check(form, field): """ diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py index 2df0ae1..8c5ad2b 100644 --- a/app/forms/pitstop.py +++ b/app/forms/pitstop.py @@ -9,13 +9,17 @@ class DeletePitStopForm(FlaskForm): class EditPitstopForm(FlaskForm): - date = DateField('Date of Pitstop', validators=[date_check]) - odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + date = DateField('Date of Pitstop') + odometer = IntegerField('Odometer (km)', validators=[edit_odometer_date_check]) litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) - costs = DecimalField('Costs (€, overall)', places=2, validators=[edit_costs_check]) + costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) submit = SubmitField(label='Update it!') last_pitstop = None same_odometer_allowed = True + pitstops = [] + + def set_pitstops(self, pitstops): + self.pitstops = pitstops def set_pitstop(self, last_pitstop): self.last_pitstop = last_pitstop @@ -33,6 +37,12 @@ class EditPitstopForm(FlaskForm): if self.costs.data: self.costs.default = self.costs.data + def get_hint_messages(self): + messages = { + 'litres': 'Litres must be higher than 0.01 L.', + 'costs': 'Costs must be higher than 0.01 €.' + } + return messages class CreatePitstopForm(FlaskForm): date = DateField('Date of Pitstop') diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 75f4e5c..252a21b 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -135,7 +135,13 @@ def edit_pit_stop_form(pid): form = EditPitstopForm() form.set_pitstop(last_pitstop) - + data = get_event_line_for_vehicle(vehicle) + form.set_pitstops(data) + if not form.is_submitted(): + form.odometer.default = edit_pitstop.odometer + form.litres.default = edit_pitstop.amount + form.date.default = edit_pitstop.date + form.costs.default = edit_pitstop.costs if form.validate_on_submit(): edit_pitstop.costs = form.costs.data edit_pitstop.date = form.date.data @@ -145,18 +151,9 @@ def edit_pit_stop_form(pid): 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.preinit_with_data() 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) + return render_template('editPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) @app.route('/pitstops', methods=['GET']) diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html index 4767128..7295c8e 100644 --- a/app/templates/pitstops.html +++ b/app/templates/pitstops.html @@ -31,10 +31,10 @@ {% if loop.first %} + {% endif %} edit - {% endif %} delete -- 2.30.2 From 6497d4677fe29c7c36364ec1787e305c2fb8b3b9 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Fri, 17 Aug 2018 07:15:27 +0200 Subject: [PATCH 15/19] Fixed a bug with editing services / pitstops The event that was edited should not limit the dates and odometers for the update. --- app/forms/pitstop.py | 5 +---- app/forms/service.py | 22 +++++++++++----------- app/routes/pitstop.py | 8 +------- app/routes/service.py | 28 ++++++++++------------------ app/templates/pitstops.html | 2 +- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/app/forms/pitstop.py b/app/forms/pitstop.py index 8c5ad2b..a8aa31c 100644 --- a/app/forms/pitstop.py +++ b/app/forms/pitstop.py @@ -14,16 +14,12 @@ class EditPitstopForm(FlaskForm): litres = DecimalField('Litres (l)', places=2, validators=[litres_check]) costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check]) submit = SubmitField(label='Update it!') - last_pitstop = None same_odometer_allowed = True pitstops = [] def set_pitstops(self, pitstops): self.pitstops = pitstops - 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) @@ -44,6 +40,7 @@ class EditPitstopForm(FlaskForm): } return messages + class CreatePitstopForm(FlaskForm): date = DateField('Date of Pitstop') odometer = IntegerField('Odometer (km)', validators=[odometer_date_check]) diff --git a/app/forms/service.py b/app/forms/service.py index bc19add..3415ead 100644 --- a/app/forms/service.py +++ b/app/forms/service.py @@ -43,33 +43,33 @@ class DeleteServiceForm(FlaskForm): class EditServiceForm(FlaskForm): - date = DateField('Date of Service', validators=[date_check]) - odometer = IntegerField('Odometer (km)', validators=[odometer_check]) + date = DateField('Date of Service') + odometer = IntegerField('Odometer (km)', validators=[edit_odometer_date_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 + pitstops = [] - def set_pitstop(self, last_pitstop): - self.last_pitstop = last_pitstop + def set_pitstops(self, pitstops): + self.pitstops = pitstops 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 if self.description.data: self.description.default = self.description.data + def get_hint_messages(self): + messages = { + 'litres': 'Litres must be higher than 0.01 L.', + 'costs': 'Costs must be higher than 0.01 €.' + } + return messages \ No newline at end of file diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 252a21b..4781660 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -127,15 +127,9 @@ def edit_pit_stop_form(pid): 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) data = get_event_line_for_vehicle(vehicle) + data = [x for x in data if x != edit_pitstop] form.set_pitstops(data) if not form.is_submitted(): form.odometer.default = edit_pitstop.odometer diff --git a/app/routes/service.py b/app/routes/service.py index 6c73520..68b6800 100644 --- a/app/routes/service.py +++ b/app/routes/service.py @@ -70,16 +70,17 @@ def edit_service_form(sid): 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) + data = [x for x in data if x != edit_service] + form = EditServiceForm() form.same_odometer_allowed = True + form.set_pitstops(data) + if not form.is_submitted(): + form.odometer.default = edit_service.odometer + form.description.default = edit_service.description + form.date.default = edit_service.date + form.costs.default = edit_service.costs if form.validate_on_submit(): edit_service.costs = form.costs.data edit_service.date = form.date.data @@ -89,18 +90,9 @@ def edit_service_form(sid): 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.preinit_with_data() 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) + return render_template('editServiceForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages()) @app.route('/service/vehicle/select', methods=['GET', 'POST']) diff --git a/app/templates/pitstops.html b/app/templates/pitstops.html index 7295c8e..db15b5e 100644 --- a/app/templates/pitstops.html +++ b/app/templates/pitstops.html @@ -73,10 +73,10 @@ {% if loop.first %} + {% endif %} edit - {% endif %} delete -- 2.30.2 From cbc34a02e748b7bf392f459de7440853f3871005 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 8 May 2021 19:32:01 +0200 Subject: [PATCH 16/19] changed base image --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d6ff606..85b1b85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM debian9_python3 +FROM python:3 COPY requirements.txt /requirements.txt -RUN pip3 install -r /requirements.txt; \ - mkdir /data +RUN mkdir /data; \ + pip3 install -r /requirements.txt; ADD app /app ADD main.py /main.py -- 2.30.2 From 7797d6c91ee19e704a897452468d3fd40738da07 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sat, 8 May 2021 19:54:05 +0200 Subject: [PATCH 17/19] add mail validator package --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 321d734..0e5305f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ Flask-WTF PyMySQL markdown Flask-Limiter -requests \ No newline at end of file +requests +email_validator -- 2.30.2 From 41961115297e0ddcb74f4f9ed3c1cf56b6f929c6 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Mon, 31 May 2021 21:23:12 +0200 Subject: [PATCH 18/19] adapt general request limits --- app/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index bb238a8..ca9660d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -15,10 +15,11 @@ from .forms import * app = Flask(__name__) app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default']) +# applies to all routes, so choose limits wisely! limiter = Limiter( app, key_func=get_remote_address, - default_limits=["200 per day", "50 per hour"] +# default_limits=["500 per second"] ) -- 2.30.2 From 42356137c42306679fb249d201de0e830930575e Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Thu, 17 Jun 2021 18:28:19 +0200 Subject: [PATCH 19/19] first shot --- README.md | 9 +- app/entities.py | 133 +++++++++++-------- app/forms/__init__.py | 1 + app/forms/checks.py | 122 +++++++++++++---- app/forms/regular_cost.py | 86 ++++++++++++ app/routes/__init__.py | 3 +- app/routes/pitstop.py | 17 ++- app/routes/regular_cost.py | 161 +++++++++++++++++++++++ app/static/js/main.js | 2 +- app/templates/createRegularCostForm.html | 39 ++++++ app/templates/deleteRegularCostForm.html | 46 +++++++ app/templates/deleteVehicleForm.html | 2 +- app/templates/editRegularCostForm.html | 39 ++++++ app/templates/endRegularCostForm.html | 16 +++ app/templates/layout.html | 18 ++- app/templates/pitstops.html | 97 ++++++++++++++ app/templates/statistics.html | 29 +++- app/tools.py | 156 ++++++++++++++++------ 18 files changed, 839 insertions(+), 137 deletions(-) create mode 100644 app/forms/regular_cost.py create mode 100644 app/routes/regular_cost.py create mode 100644 app/templates/createRegularCostForm.html create mode 100644 app/templates/deleteRegularCostForm.html create mode 100644 app/templates/editRegularCostForm.html create mode 100644 app/templates/endRegularCostForm.html diff --git a/README.md b/README.md index f1d2876..31087c6 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,20 @@ ## general configuration -Look at *app/config/email.py.example** for the configuration of the +Look at *app/config/email.py.example* for the configuration of the parameters required for sending emails. Copy the file as *email.py* to a folder that will serve as configuration directory and fill in the information. The directory will be used as volume during container operation. ## start database + `docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mysql:latest` +or + +`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mariadb:latest` + ## Database migrations ### From *Cathrine* to *Master*: @@ -26,6 +31,6 @@ reloaded automatically. The sqlite file will be stored in *tmp* so it can be inspected with tools like *sqlite3*. The switch *DEBUG* enables debugging during development. -`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app --link pitstops_db:database -p 5000:5000 -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch` +`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/data:/data -p 5000:5000 -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch` ## run in production `docker run --name pitstops -tid -e PROXY_DATA=server_names:ps.lusiardi.de,port:5000 --link pitstops_db:database -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch:catherine` diff --git a/app/entities.py b/app/entities.py index d21fc0b..b7d04ee 100644 --- a/app/entities.py +++ b/app/entities.py @@ -1,24 +1,33 @@ from app import db from flask_security import UserMixin, RoleMixin -roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), - db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) +roles_users = db.Table( + "roles_users", + db.Column("user_id", db.Integer(), db.ForeignKey("user.id")), + db.Column("role_id", db.Integer(), db.ForeignKey("role.id")), +) -vehicles_consumables = db.Table('vehicles_consumables', - db.Column('vehicle_id', db.Integer(), db.ForeignKey('vehicle.id')), - db.Column('consumable_id', db.Integer(), db.ForeignKey('consumable.id'))) +vehicles_consumables = db.Table( + "vehicles_consumables", + db.Column("vehicle_id", db.Integer(), db.ForeignKey("vehicle.id")), + db.Column("consumable_id", db.Integer(), db.ForeignKey("consumable.id")), +) -users_fillingstations = db.Table('users_fillingstations', - db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), - db.Column('fillingstation_id', db.Integer(), db.ForeignKey('filling_station.int_id'))) +users_fillingstations = db.Table( + "users_fillingstations", + db.Column("user_id", db.Integer(), db.ForeignKey("user.id")), + db.Column( + "fillingstation_id", db.Integer(), db.ForeignKey("filling_station.int_id") + ), +) class Role(db.Model, RoleMixin): """ Entity to handle different roles for users: Typically user and admin exist """ + id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(80), unique=True) description = db.Column(db.String(255)) @@ -34,6 +43,7 @@ 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)) @@ -43,17 +53,12 @@ class User(db.Model, UserMixin): home_long = db.Column(db.Numeric(8, 5), default=0) home_zoom = db.Column(db.Integer(), default=0) - vehicles = db.relationship( - 'Vehicle' - ) + vehicles = db.relationship("Vehicle") roles = db.relationship( - 'Role', - secondary=roles_users, - backref=db.backref('users', lazy='dynamic') + "Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic") ) favourite_filling_stations = db.relationship( - 'FillingStation', - secondary=users_fillingstations + "FillingStation", secondary=users_fillingstations ) def __repr__(self): @@ -69,31 +74,26 @@ class Vehicle(db.Model): * 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')) + owner_id = db.Column(db.Integer, db.ForeignKey("user.id")) name = db.Column(db.String(255)) - pitstops = db.relationship( - 'Pitstop', - order_by="asc(Pitstop.odometer)" - ) - services = db.relationship( - 'Service', - order_by = "asc(Service.odometer)" - ) - consumables = db.relationship( - 'Consumable', - secondary=vehicles_consumables - ) + pitstops = db.relationship("Pitstop", order_by="asc(Pitstop.odometer)") + services = db.relationship("Service", order_by="asc(Service.odometer)") + regulars = db.relationship("RegularCost") + consumables = db.relationship("Consumable", secondary=vehicles_consumables) # allow vehicle names to be duplicated between different owners but must still be uniq for each owner - __table_args__ = (db.UniqueConstraint('owner_id', - 'name', - name='_owner_name_uniq'),) + __table_args__ = (db.UniqueConstraint("owner_id", "name", name="_owner_name_uniq"),) def __init__(self, name): self.name = name def __repr__(self): - return '' % (self.id, self.owner_id, self.name) + return '' % ( + self.id, + self.owner_id, + self.name, + ) class Pitstop(db.Model): @@ -107,21 +107,26 @@ class Pitstop(db.Model): * 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_id = db.Column(db.Integer, db.ForeignKey("consumable.id")) amount = db.Column(db.Numeric(5, 2)) costs = db.Column(db.Numeric(5, 2), default=0) - vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id')) + 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'),) + 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, amount, date, costs, consumable_id): self.odometer = odometer @@ -131,8 +136,31 @@ class Pitstop(db.Model): self.consumable_id = consumable_id def __repr__(self): - return '' % \ - (self.odometer, self.amount, self.date, self.vehicle_id, self.consumable_id) + return '' % ( + self.odometer, + self.amount, + self.date, + self.vehicle_id, + self.consumable_id, + ) + + +class RegularCost(db.Model): + id = db.Column(db.Integer, primary_key=True) + vehicle_id = db.Column(db.Integer, db.ForeignKey("vehicle.id")) + description = db.Column(db.String(4096), unique=True) + costs = db.Column(db.Numeric(10, 2), default=0) + days = db.Column(db.String(1024)) + start_at = db.Column(db.Date) + ends_at = db.Column(db.Date) + + def __init__(self, vehicle_id, description, costs, days, start_at, ends_at): + self.vehicle_id = vehicle_id + self.description = description + self.costs = costs + self.days = days + self.start_at = start_at + self.ends_at = ends_at class Consumable(db.Model): @@ -142,15 +170,13 @@ class Consumable(db.Model): * name (must be globally unique) * unit """ + id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), unique=True) ext_id = db.Column(db.String(255)) unit = db.Column(db.String(255)) - vehicles = db.relationship( - 'Vehicle', - secondary=vehicles_consumables - ) + vehicles = db.relationship("Vehicle", secondary=vehicles_consumables) def __init__(self, name, ext_id, unit): self.name = name @@ -165,7 +191,7 @@ 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')) + 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)) @@ -177,8 +203,10 @@ class Service(db.Model): self.vehicle_id = vehicle_id def __repr__(self): - return '' % \ - (self.odometer, self.date, self.vehicle_id, self.costs, self.description) + return ( + '' + % (self.odometer, self.date, self.vehicle_id, self.costs, self.description) + ) class FillingStation(db.Model): @@ -203,6 +231,7 @@ class FillingStation(db.Model): for c in self.__table__.columns: val = getattr(self, c.name) import decimal + if isinstance(val, decimal.Decimal): val = float(val) val = str(val) diff --git a/app/forms/__init__.py b/app/forms/__init__.py index e4f65bc..6a83e2c 100644 --- a/app/forms/__init__.py +++ b/app/forms/__init__.py @@ -4,3 +4,4 @@ from .checks import * from .consumable import * from .service import * from .vehicle import * +from .regular_cost import * diff --git a/app/forms/checks.py b/app/forms/checks.py index a7b827f..8efe182 100644 --- a/app/forms/checks.py +++ b/app/forms/checks.py @@ -2,10 +2,34 @@ from wtforms.validators import ValidationError from datetime import date +def regular_costs_days_check(form, field): + """ + Checks the input field to enter multiple days in the following format: + `01-15,07-15` for Jan 15th and July 15 + """ + days = form.days.data + for day in days.split(","): + day = day.strip() + if not day: + raise ValidationError("Missing Date after ','") + try: + m, d = day.split("-") + m_i = int(m) + d_i = int(d) + except Exception: + raise ValidationError("Malformed Date, must be 'Month-Day'") + try: + d = date(2021, m_i, d_i) + except Exception: + raise ValidationError("{}-{} is not a valid date".format(m, d)) + + def odometer_date_check(form, field): """ - Checks that the entered date and odometer of the pit stop is conformant to the existing pit stops. That means, if a - pitstops date is between two other pit stops, the odometer should be as well. + Checks that the entered date and odometer of the pit stop is conformant to + the existing pit stops. That means, if a pitstops date is between two + other pit stops, the odometer should be as well. + :param form: :param field: :return: @@ -16,23 +40,37 @@ def odometer_date_check(form, field): if len(pitstops) > 0: if date < pitstops[0].date and odometer >= pitstops[0].odometer: - raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) + raise ValidationError( + "The new odometer value must be less than %i km" + % pitstops[0].odometer + ) if date >= pitstops[-1].date and odometer <= pitstops[-1].odometer: - raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) + raise ValidationError( + "The new odometer value must be greater than %i km" + % pitstops[-1].odometer + ) if len(pitstops) > 1: - for index in range(0, len(pitstops)-1): + for index in range(0, len(pitstops) - 1): if pitstops[index].date <= date < pitstops[index + 1].date: - if odometer <= pitstops[index].odometer or odometer >= pitstops[index+1].odometer: - raise ValidationError('The new odometer value must be greater than %i km and less than %i km' - % (pitstops[index].odometer,pitstops[index+1].odometer)) + if ( + odometer <= pitstops[index].odometer + or odometer >= pitstops[index + 1].odometer + ): + raise ValidationError( + "The new odometer value must be greater than %i km and less than %i km" + % ( + pitstops[index].odometer, + pitstops[index + 1].odometer, + ) + ) def edit_odometer_date_check(form, field): """ - This makes exactly the same checks as 'odometer_date_check' but the odometers may be the same (to change only amount - and price). + This makes exactly the same checks as 'odometer_date_check' but the + odometers may be the same (to change only amount and price). :param form: :param field: @@ -44,31 +82,50 @@ def edit_odometer_date_check(form, field): if len(pitstops) > 0: if date < pitstops[0].date and odometer > pitstops[0].odometer: - raise ValidationError('The new odometer value must be less than %i km' % pitstops[0].odometer) + raise ValidationError( + "The new odometer value must be less than %i km" + % pitstops[0].odometer + ) if date >= pitstops[-1].date and odometer < pitstops[-1].odometer: - raise ValidationError('The new odometer value must be greater than %i km' % pitstops[-1].odometer) + raise ValidationError( + "The new odometer value must be greater than %i km" + % pitstops[-1].odometer + ) if len(pitstops) > 1: - for index in range(0, len(pitstops)-1): + for index in range(0, len(pitstops) - 1): if pitstops[index].date <= date < pitstops[index + 1].date: - if odometer < pitstops[index].odometer or odometer > pitstops[index+1].odometer: - raise ValidationError('The new odometer value must be greater than %i km and less than %i km' - % (pitstops[index].odometer,pitstops[index+1].odometer)) + if ( + odometer < pitstops[index].odometer + or odometer > pitstops[index + 1].odometer + ): + raise ValidationError( + "The new odometer value must be greater than %i km and less than %i km" + % ( + pitstops[index].odometer, + pitstops[index + 1].odometer, + ) + ) 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. + 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) + 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()) + raise ValidationError( + "The new date must not be after %s" % date.today() + ) def odometer_check(form, field): @@ -78,20 +135,29 @@ def odometer_check(form, field): :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 ( + 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) + 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') + 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 €.') + raise ValidationError("Costs must be above 0.01 €.") def edit_costs_check(form, field): @@ -101,8 +167,8 @@ def edit_costs_check(form, field): :param field: :return: """ - costs_check_required = (form.costs.default is not None and form.costs.default > 0) + 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 €.') - - + raise ValidationError("Costs must be above 0.01 €.") diff --git a/app/forms/regular_cost.py b/app/forms/regular_cost.py new file mode 100644 index 0000000..c8009e9 --- /dev/null +++ b/app/forms/regular_cost.py @@ -0,0 +1,86 @@ +from flask_wtf import FlaskForm +from wtforms import ( + DateField, + IntegerField, + DecimalField, + SubmitField, + TextAreaField, + StringField, +) +from wtforms.validators import Length + +from .checks import * + +from wtforms.validators import Optional + + +class DeleteRegularCostForm(FlaskForm): + submit = SubmitField(label="Really delete this regular cost!") + + +class EndRegularCostForm(FlaskForm): + submit = SubmitField(label="Really end this regular cost!") + + +class EditRegularCostForm(FlaskForm): + start_at = DateField("Date of first instance") + ends_at = DateField( + "Date of last instance", validators=[Optional(strip_whitespace=True)] + ) + + days = StringField("Days for instance", validators=[regular_costs_days_check]) + + costs = DecimalField("Costs (€, per instance)", places=2, validators=[costs_check]) + description = TextAreaField("Description", validators=[Length(1, 4096)]) + submit = SubmitField(label="Update it!") + + def preinit_with_data(self): + if self.costs.data: + self.costs.default = self.costs.data + if self.start_at.data: + self.start_at.default = self.start_at.data + if self.ends_at.data: + self.ends_at.default = self.ends_at.data + if self.days.data: + self.days.default = self.days.data + if self.description.data: + self.description.default = self.description.data + + def get_hint_messages(self): + messages = {"costs": "Costs must be higher than 0.01 €."} + return messages + + +class CreateRegularCostForm(FlaskForm): + start_at = DateField("Date of first instance") + ends_at = DateField( + "Date of last instance", validators=[Optional(strip_whitespace=True)] + ) + + days = StringField("Days for instance", validators=[regular_costs_days_check]) + + costs = DecimalField("Costs (€, per instance)", places=2, validators=[costs_check]) + description = TextAreaField("Description", validators=[Length(1, 4096)]) + submit = SubmitField(label="Do it!") + + def preinit_with_data(self): + if self.start_at.data: + self.start_at.default = self.start_at.data + else: + self.start_at.default = date.today() + + if self.ends_at.data: + self.ends_at.default = self.ends_at.data + else: + self.ends_at.default = None + + if self.days.data: + self.days.default = self.days.data + + if self.costs.data: + self.costs.default = self.costs.data + else: + self.costs.default = 0 + + if self.description.data: + self.description.default = self.description.data diff --git a/app/routes/__init__.py b/app/routes/__init__.py index c169592..00e44b6 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -3,4 +3,5 @@ from .admin import * from .misc import * from .pitstop import * from .service import * -from .filling_stations import * \ No newline at end of file +from .filling_stations import * +from .regular_cost import * diff --git a/app/routes/pitstop.py b/app/routes/pitstop.py index 4781660..c9b8446 100644 --- a/app/routes/pitstop.py +++ b/app/routes/pitstop.py @@ -3,11 +3,16 @@ from flask_security import login_required from flask_security.core import current_user from sqlalchemy.exc import IntegrityError from datetime import date +import types + 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, pitstop_service_key, \ - get_event_line_for_vehicle, update_filling_station_prices +from ..forms import SelectVehicleForm, SelectConsumableForm, \ + CreatePitstopForm, EditPitstopForm, DeletePitStopForm +from ..tools import db_log_update, db_log_delete, db_log_add, \ + pitstop_service_key, get_event_line_for_vehicle, \ + update_filling_station_prices, RegularCostInstance, \ + calculate_regular_cost_instances from .. import app, db @@ -162,11 +167,15 @@ def get_pit_stops(): data.append(pitstop) for service in vehicle.services: data.append(service) + for regular_instance in calculate_regular_cost_instances(vehicle): + data.append(regular_instance) + data.sort(key=pitstop_service_key) v = { 'id': vehicle.id, 'name': vehicle.name, - 'data': data + 'data': data, + "regulars": vehicle.regulars, } user['vehicles'].append(v) diff --git a/app/routes/regular_cost.py b/app/routes/regular_cost.py new file mode 100644 index 0000000..e0d1b24 --- /dev/null +++ b/app/routes/regular_cost.py @@ -0,0 +1,161 @@ +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, RegularCost +from ..forms import ( + SelectVehicleForm, + CreateRegularCostForm, + DeleteRegularCostForm, + EditRegularCostForm, + EndRegularCostForm, +) +from ..tools import ( + db_log_update, + db_log_delete, + db_log_add, + pitstop_service_key, + get_event_line_for_vehicle, + update_filling_station_prices, +) +from .. import app, db + + +@app.route("/regular_costs/delete/", methods=["GET", "POST"]) +@login_required +def delete_regular_form(pid): + regular_cost = RegularCost.query.filter(RegularCost.id == pid).first() + if regular_cost is None: + return redirect(url_for("get_pit_stops")) + vehicle = Vehicle.query.filter(Vehicle.id == regular_cost.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for("get_pit_stops")) + + form = DeleteRegularCostForm() + if form.validate_on_submit(): + db.session.delete(regular_cost) + db.session.commit() + db_log_delete(regular_cost) + return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id))) + + return render_template( + "deleteRegularCostForm.html", form=form, regular_cost=regular_cost + ) + + +@app.route("/regular_costs/vehicle/select", methods=["GET", "POST"]) +@login_required +def select_vehicle_for_new_regular_cost(): + if len(current_user.vehicles) == 1: + return redirect( + url_for("create_regular_cost_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_regular_cost_for_vehicle", vid=form.vehicle.data) + ) + + return render_template("selectVehicle.html", form=form) + + +@app.route("/regular_costs/vehicle//create", methods=["GET", "POST"]) +@login_required +def create_regular_cost_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 = CreateRegularCostForm() + + form.preinit_with_data() + + if form.validate_on_submit(): + regular_cost = RegularCost( + vid, + form.description.data, + form.costs.data, + form.days.data, + form.start_at.data, + form.ends_at.data, + ) + db.session.add(regular_cost) + vehicle.regulars.append(regular_cost) + db.session.commit() + return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id))) + + form.process() + return render_template( + "createRegularCostForm.html", form=form, vehicle=vehicle, messages=[] + ) + + +@app.route("/regular_costs/edit/", methods=["GET", "POST"]) +@login_required +def edit_regular_form(pid): + edit_regular = RegularCost.query.get(pid) + if edit_regular is None: + return redirect(url_for("get_pit_stops")) + + vehicle = Vehicle.query.filter(Vehicle.id == edit_regular.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for("get_pit_stops")) + + form = EditRegularCostForm() + form.preinit_with_data() + + if not form.is_submitted(): + form.costs.default = edit_regular.costs + form.start_at.default = edit_regular.start_at + form.ends_at.default = edit_regular.ends_at + form.days.default = edit_regular.days + form.description.default = edit_regular.description + + if form.validate_on_submit(): + edit_regular.start_at = form.start_at.data + edit_regular.ends_at = form.ends_at.data + edit_regular.costs = form.costs.data + edit_regular.days = form.days.data + edit_regular.description = form.description.data + db.session.commit() + db_log_update(edit_regular) + return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id))) + + form.preinit_with_data() + form.process() + return render_template( + "editRegularCostForm.html", + form=form, + vehicle=vehicle, + messages=form.get_hint_messages(), + ) + + +@app.route("/regular_costs/end/", methods=["GET", "POST"]) +@login_required +def end_regular_form(pid): + edit_regular = RegularCost.query.get(pid) + if edit_regular is None: + return redirect(url_for("get_pit_stops")) + + vehicle = Vehicle.query.filter(Vehicle.id == edit_regular.vehicle_id).first() + if vehicle not in current_user.vehicles: + return redirect(url_for("get_pit_stops")) + + form = EndRegularCostForm() + if form.validate_on_submit(): + edit_regular.ends_at = date.today() + db.session.commit() + return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id))) + + return render_template( + "endRegularCostForm.html", + form=form, + vehicle=vehicle, + ) + diff --git a/app/static/js/main.js b/app/static/js/main.js index 4f1b963..1d594a4 100644 --- a/app/static/js/main.js +++ b/app/static/js/main.js @@ -12,7 +12,7 @@ function createChart(id, data, unit) { "axisAlpha": 0, "position": "left", "ignoreAxisWidth":true, -// "title": unit + "title": unit }], "balloon": { "borderThickness": 1, diff --git a/app/templates/createRegularCostForm.html b/app/templates/createRegularCostForm.html new file mode 100644 index 0000000..3870fcd --- /dev/null +++ b/app/templates/createRegularCostForm.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

New Regular Cost for '{{ vehicle.name }}'

+
+ {{ form.hidden_tag() }} + + {{ render_field_with_errors(form.start_at) }} + + {{messages['start_at']}} + + + {{ render_field_with_errors(form.ends_at) }} + + {{messages['ends_at']}} + + + {{ render_field_with_errors(form.days) }} + Format as 'Month-Day' (e.g. 05-25) and separate with ','. + + {{messages['days']}} + + + {{ 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/deleteRegularCostForm.html b/app/templates/deleteRegularCostForm.html new file mode 100644 index 0000000..9ccf944 --- /dev/null +++ b/app/templates/deleteRegularCostForm.html @@ -0,0 +1,46 @@ +{% extends 'layout.html' %} + +{% block body %} +
+
+
+
+

Delete Regular Cost?

+ + + + + + + + + + + + + + + + + + + + + +
Description of regular cost{{ regular_cost.description }}
Costs (per instance) + {% if regular_cost.costs %} + {{ regular_cost.costs }} + {% else %} + -- + {% endif %} + € +
Days for regular costs{{ regular_cost.days }}
regular costs starting from{{ regular_cost.start_at }}
regular costs ending at{{ regular_cost.ends_at }}
+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.submit) }} +
+
+
+
+
+{% endblock %} diff --git a/app/templates/deleteVehicleForm.html b/app/templates/deleteVehicleForm.html index c058771..d8694b3 100644 --- a/app/templates/deleteVehicleForm.html +++ b/app/templates/deleteVehicleForm.html @@ -8,7 +8,7 @@

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

{{ form.hidden_tag() }} - {{ render_field_with_errors(form.submit) }} + {{ render_field_with_errors(form.submit, include_cancel=True) }}
diff --git a/app/templates/editRegularCostForm.html b/app/templates/editRegularCostForm.html new file mode 100644 index 0000000..599e570 --- /dev/null +++ b/app/templates/editRegularCostForm.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

Edit Regular Cost for '{{ vehicle.name }}'

+
+ {{ form.hidden_tag() }} + + {{ render_field_with_errors(form.start_at) }} + + {{messages['start_at']}} + + + {{ render_field_with_errors(form.ends_at) }} + + {{messages['ends_at']}} + + + {{ render_field_with_errors(form.days) }} + Format as 'Month-Day' (e.g. 05-25) and separate with ','. + + {{messages['days']}} + + + {{ 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/endRegularCostForm.html b/app/templates/endRegularCostForm.html new file mode 100644 index 0000000..483fb58 --- /dev/null +++ b/app/templates/endRegularCostForm.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} + +{% block body %} +
+
+
+
+

Edit Regular Cost for '{{ vehicle.name }}'

+
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.submit) }} +
+
+
+
+{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index 4295654..dc6582e 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -3,6 +3,7 @@
  • Plan Pitstop
  • Create Pitstop
  • Create Service
  • +
  • Create Regular Cost
  • Statistics
  • Account
  • {% if current_user.has_role('admin') %} @@ -15,20 +16,17 @@ {% endif %} {%- endmacro %} -{% macro render_field_with_errors(field) %} +{% macro render_field_with_errors(field, include_cancel=True) %}
    {% if field.type == 'SubmitField' %} -
    - -
    +
    +
    + {% if include_cancel %} + + {% endif %}
    - -
    +
    {% else %}
    {% endmacro %} @@ -115,6 +130,7 @@ {{ print_vehicle_table(vehicle) }}