Compare commits

...

186 Commits

Author SHA1 Message Date
Joachim Lusiardi bc7e1591bd fix sorting for regular costs 2021-06-23 08:29:35 +02:00
Joachim Lusiardi 673b671ab8 fix date of service 2021-06-22 21:49:32 +02:00
Joachim Lusiardi 578b2c15d8 bugfix: could not create service 2021-06-22 21:39:49 +02:00
jlusiardi 5cd06db1b6 Merge pull request 'vehicles can now be deactivated' (#6) from vehicles_can_be_deactivated into master
Reviewed-on: #6
2021-06-20 06:49:33 +00:00
Joachim Lusiardi 3035006225 vehicles can now be deactivated 2021-06-20 08:43:46 +02:00
jlusiardi 3d0bb8c83f Merge pull request 'regular_costs' (#5) from regular_costs into master
Reviewed-on: #5
2021-06-18 18:20:12 +00:00
Joachim Lusiardi 42356137c4 first shot 2021-06-18 19:25:33 +02:00
Joachim Lusiardi 4196111529 adapt general request limits 2021-05-31 21:30:54 +02:00
Joachim Lusiardi 7797d6c91e add mail validator package 2021-05-08 19:54:05 +02:00
Joachim Lusiardi cbc34a02e7 changed base image 2021-05-08 19:32:01 +02:00
Joachim Lusiardi 6497d4677f Fixed a bug with editing services / pitstops
The event that was edited should not limit the dates and
odometers for the update.
2018-08-17 07:15:27 +02:00
Joachim Lusiardi 1d767d65a6 all pit stops can now be edited and deleted
not only the last pit stop can be edited and deleted.
2018-08-16 18:33:09 +02:00
Joachim Lusiardi ae6dfa8ae6 Fix bug with description on services
The service description was lost on any input error in the forms
for create and edit.
2018-08-16 06:58:11 +02:00
Joachim Lusiardi 346723ae2d Makes each service/pitstop deletable
adds a delete button to each service/pitstop
2018-08-15 21:46:31 +02:00
Joachim Lusiardi 6397d050ae 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.
2018-08-15 21:45:22 +02:00
Joachim Lusiardi bf4bb607d5 Introduces ordering for services
services are now ordered by ascending odometer value instead of
their id
2018-08-15 21:39:04 +02:00
Joachim Lusiardi b3cb88e870 fix issue with creating the first ever pitstop for vehicle 2018-08-11 19:38:51 +02:00
Joachim Lusiardi 889a3e6476 changes dev config to store sqlite file in data volume 2018-08-11 18:58:25 +02:00
Joachim Lusiardi 3a2b25d3bd fixes issue with first pitstop for a vehicle 2018-08-11 18:57:33 +02:00
Joachim Lusiardi 13090d2735 Updates create pitstop form
The form now uses the new odometer_date_check to allow entering
pitstops that are before the currently last pitstop.
2018-08-11 14:53:08 +02:00
Joachim Lusiardi 25705562ad Displays informative messages on litre amount 2018-08-11 14:13:03 +02:00
Joachim Lusiardi 1f2e33bec4 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.
2018-08-11 14:11:09 +02:00
Joachim Lusiardi c42a667ca4 Introduces ordering for pitstops
Pitstops are now ordered by ascending odometer value instead of
their id
2018-08-11 13:15:33 +02:00
Joachim Lusiardi 879669b4f7 Updates os version to debian 9
Changes to debian9_python3 image because of outdated setup tools
Fixed a typo as well
2018-08-11 13:06:07 +02:00
Joachim Lusiardi 5a961d096b add tooltips to buttons for filling stations 2018-08-10 20:16:36 +02:00
Joachim Lusiardi d9812bcb06 add links to imprint and dsgvo declaration 2018-05-09 12:41:40 +02:00
Joachim Lusiardi 8b78dbe5b7 add DEBUG = False to default config 2017-11-25 11:12:16 +01:00
Joachim Lusiardi 95a7e56a32 load openlayers via https 2017-11-25 11:11:55 +01:00
Joachim Lusiardi 9eb4d5ada0 fixup! preparation commit for henni 2017-11-24 09:08:37 +01:00
Joachim Lusiardi f7cb273254 preparation commit for henni
This commit includes:
* sql conversion script
* fixes for the column type for lat and lng fields
* removes prints / console logs
* unneccessary comments
* if no home data is available, we center to germany
* hide non working brand logos
* on closed stations the word "closed" is printed instead of outdated
price
* updates on filling stations is done in chunks of max 10
* configuration separates between development and testing
2017-11-24 09:08:04 +01:00
Joachim Lusiardi 543d3e1658 react to closed stations 2017-11-12 22:50:25 +01:00
Joachim Lusiardi 9427ed50ad add page to plan a pitstop 2017-11-12 15:38:32 +01:00
Joachim Lusiardi 28990f27fa moved static files to separate directories 2017-11-12 15:34:49 +01:00
Joachim Lusiardi d55fd3d9f6 Consumables can now be linked to the tankerkoenig api 2017-11-12 10:17:36 +01:00
Joachim Lusiardi e9bb7986f4 moves routes for filling stations to extra file 2017-11-12 09:44:51 +01:00
Joachim Lusiardi eaacd3f42e adds functionality to handle favourit filling stations 2017-11-10 10:38:15 +01:00
Joachim Lusiardi 8a99e4a616 adds icons & logos
adds icons for filling stations and favorites
adds logos for various brands
2017-11-10 10:37:39 +01:00
Joachim Lusiardi 028b52d12f Adds setting home in account 2017-11-07 22:33:14 +01:00
Joachim Lusiardi 958a9bdd9f DEBUG flag can now be set via config and environment 2017-11-07 22:31:31 +01:00
Joachim Lusiardi b02938cafe Moves MySQL DB to production 2017-11-07 22:30:44 +01:00
Joachim Lusiardi 47e81c7517 start implementing a proxy layer for the API of creativecommons.tankerkoenig.de 2017-10-30 06:49:56 +01:00
Joachim Lusiardi af6f271b29 Merge branch 'development' into 'master'
Development

See merge request !30
2016-11-15 21:42:08 +01:00
Joachim Lusiardi c9b1a1b095 Merge branch 'special_costs' into 'development'
Special costs

See merge request !29
2016-11-15 21:33:10 +01:00
Joachim Lusiardi 2e4c815a10 Add of db update script 2016-11-15 21:31:37 +01:00
Joachim Lusiardi 5859763a14 Fixed deprecation warning for Form 2016-11-06 18:53:13 +01:00
Joachim Lusiardi 6e665b9f2f finished handling of services 2016-11-06 13:22:54 +01:00
Joachim Lusiardi 60a1b307e4 changes to beautify the generated markdown 2016-11-06 13:14:28 +01:00
Joachim Lusiardi 3f458937d9 adds markdown filter 2016-11-06 13:14:03 +01:00
Joachim Lusiardi bceb9181ee sorting for events 2016-11-01 19:48:24 +01:00
Joachim Lusiardi 086c643365 fixed shadowing warning 2016-11-01 19:48:08 +01:00
Joachim Lusiardi d8c6a0a59b splitted routes into package 2016-11-01 18:01:52 +01:00
Joachim Lusiardi 43d9fc412e moved forms to extra package 2016-11-01 16:36:28 +01:00
Joachim Lusiardi 71412bf487 integrate services into rollerverbrauch 2016-11-01 11:19:26 +01:00
Joachim Lusiardi dd11419305 add the crud forms for services 2016-11-01 11:18:40 +01:00
Joachim Lusiardi d132dc72de Add Entitiy for the Service Object 2016-11-01 11:17:54 +01:00
Joachim Lusiardi 43fa41aace Add input type TextAreaField
adds a possibility to add longer texts in the style of
rollerverbrauch
2016-11-01 11:16:13 +01:00
Joachim Lusiardi b8a6f69090 add gitattributes to handle line ending settings and expose ports on docker compose 2016-08-28 15:45:08 +02:00
Joachim Lusiardi bb7c2780e8 Merge branch '11_Redirect_to_edit_vehicle_page_if_no_consumable_is_configured' into 'development'
11 redirect to edit vehicle page if no consumable is configured



See merge request !28
2016-08-28 14:01:37 +02:00
Joachim Lusiardi 088d1f24a0 add redirect if vehicle is not configured and flash messages 2016-08-16 13:21:54 +02:00
Joachim Lusiardi 87b3b02f3e add sqlite files to gitignore 2016-08-07 13:19:26 +02:00
Joachim Lusiardi c8a9404bf6 fixed use of os.environ 2016-08-07 13:18:39 +02:00
Joachim Lusiardi 4acf36112c add ids to pitstop elements 2016-08-07 13:18:04 +02:00
Joachim Lusiardi 4b73699750 Added ids to important elements 2016-08-06 10:59:20 +02:00
Joachim Lusiardi d393f19b47 Fixed wrong text 2016-08-06 10:59:03 +02:00
Joachim Lusiardi 44b92e58b5 use config variable to identify admin account while it is created 2016-08-03 08:13:39 +02:00
Joachim Lusiardi 1e7db29de9 add config variable to identify admin account while it is created 2016-08-03 08:12:06 +02:00
Joachim Lusiardi 938a7fc140 Removed dependency to mysql for testing 2016-08-03 08:09:58 +02:00
Joachim Lusiardi 49a45ba8a4 Merge branch 'development' into 'selenium_tests'
Sync Development to selenium tests



See merge request !27
2016-07-30 09:23:14 +02:00
Joachim Lusiardi adf51c85f2 Merge branch 'development' into 'master'
Development

Fixes:
 * wrong date preselected when creation a pitstop
 * creation of new vehicle

See merge request !26
2016-07-29 21:50:52 +02:00
Joachim Lusiardi 48d7c108f1 Merge branch '9-creation-of-new-vehicle-fails' into 'development'
Resolve "Creation of new vehicle fails"

Closes #9

See merge request !25
2016-07-29 21:12:30 +02:00
Joachim Lusiardi 7129e676e2 Fixed issue 9 2016-07-29 21:10:58 +02:00
Joachim Lusiardi aaa94f08fa Merge branch 'issue_8_wrong_date_on_creating_new_pitstop' into 'development'
fix issue 8



See merge request !24
2016-07-29 21:02:43 +02:00
Joachim Lusiardi 92fb25c1c1 fix issue 8 2016-07-25 18:54:27 +02:00
Joachim Lusiardi 5fa710149d Merge branch 'development' into 'master'
Merge preparation for Esther



See merge request !23
2016-07-24 11:26:34 +02:00
Joachim Lusiardi c815c2b09a Merge branch 'style_consumable_selection' into 'development'
Style consumable selection



See merge request !22
2016-07-23 13:14:05 +02:00
Joachim Lusiardi 529f9a6c64 Fixed problem with preselected values 2016-07-23 13:09:38 +02:00
Joachim Lusiardi bebccf0a36 Merge branch 'issue_7_really_delete_pitstop_page_does_not_show_all_information' into 'development'
Fix issue with missing information on pitstop delete page



See merge request !21
2016-07-21 19:14:45 +02:00
Joachim Lusiardi 89ebab29c4 Merge branch 'issue_6_pitstop_is_not_editable' into 'development'
Fixed the edit function



See merge request !20
2016-07-21 19:14:21 +02:00
Joachim Lusiardi a4ecae4700 Fix issue with missing information on pitstop delete page 2016-07-21 19:11:32 +02:00
Joachim Lusiardi 88f88591a9 Fixed the edit function 2016-07-21 19:03:04 +02:00
Joachim Lusiardi 473c2a4f5b Slight style improvement 2016-07-18 21:30:06 +02:00
Joachim Lusiardi bcc0541be3 Merge branch 'development' into selenium_tests 2016-07-17 10:09:01 +02:00
Joachim Lusiardi fd685c316d Update README.md 2016-07-17 10:06:19 +02:00
Joachim Lusiardi 112ed38dbd fixed SQL Error with MySQL 5.7.12 2016-07-17 10:03:56 +02:00
Joachim Lusiardi 2f737c1715 fixed DB URL 2016-07-17 10:02:41 +02:00
Joachim Lusiardi 5a447763d0 Merge branch 'development' into 'master'
Merge branch 'development' into 'master'

Merge preparation for  Danielle

This adds the following features:
 * Add accounting for additional consumable materials (#5)
 * Add cost field to pit stop (#4)
 * Make last pit stop editable / deletable (#3)

See merge request !19
2016-07-17 09:40:54 +02:00
Joachim Lusiardi cfb9dfabc0 Merge branch 'issue_5_more_consumable_material' into development
Conflicts:
	app/__init__.py
2016-07-17 09:22:38 +02:00
Joachim Lusiardi c0f6ef7b4f Merge branch 'pycharm' into 'development'
Fixed project structure for pycharm



See merge request !16
2016-07-17 08:10:04 +02:00
Joachim Lusiardi fdb8993ac3 Merge branch 'issue_5_more_consumable_material' into pycharm
Conflicts:
	app/__init__.py
	app/rollerverbrauch/templates/createPitStopForm.html
	app/rollerverbrauch/templates/newPitStopForm.html
	app/rollerverbrauch/templates/selectVehice.html
	app/rollerverbrauch/templates/selectVehicle.html
	app/templates/newPitStopForm.html
	app/templates/selectVehice.html
2016-07-16 19:29:25 +02:00
Joachim Lusiardi 1f5cb6b1fa Fixed issue with status on non-logged in index page 2016-07-16 13:29:32 +02:00
Joachim Lusiardi 2a524ffdbf Merge branch 'issue_5_more_consumable_material' into 'development'
Issue 5 more consumable material



See merge request !17
2016-07-16 12:12:43 +02:00
Joachim Lusiardi 1516ac2c51 db update script and some fixes 2016-07-16 12:07:01 +02:00
Joachim Lusiardi aa9fda93c0 statistics 3rd 2016-07-11 22:15:23 +02:00
Joachim Lusiardi ca5bd9bb02 statistics 2nd step 2016-07-11 06:59:34 +02:00
Joachim Lusiardi 4ea42a7efd first step for statistics 2016-07-10 13:40:49 +02:00
Joachim Lusiardi 43e3f9c98b . 2016-07-04 20:19:59 +02:00
Joachim Lusiardi 738bcb9f89 Fixed project structure for pycharm 2016-07-03 19:29:30 +02:00
Joachim Lusiardi af5d4ae6b3 cann add pitstops for selected vehicles / consumables 2016-06-29 22:50:01 +02:00
Joachim Lusiardi 907d0435d1 consumables for vehicles can be edited 2016-06-28 23:33:24 +02:00
Joachim Lusiardi fe4236eead renamed template for create a pitstop 2016-06-28 21:35:04 +02:00
Joachim Lusiardi a0be122186 reworked the crud funtions for vehicles 2016-06-27 22:09:42 +02:00
Joachim Lusiardi fa4126be66 adds update & delete for consumables
adds functionality to
* update
* delete
for consumables
2016-06-27 21:35:00 +02:00
Joachim Lusiardi 60f2e9f4e4 adds form to create a consumable 2016-06-27 20:08:49 +02:00
Joachim Lusiardi 5e24b9779e makes the name of a consumable unique
makes the column unique and extends the create form to respect the
uniqueness.
2016-06-27 19:48:04 +02:00
Joachim Lusiardi 90639a757d Merge branch 'development' into issue_5_more_consumable_material 2016-06-26 08:27:14 +02:00
Joachim Lusiardi 820063837d Merge branch 'issue_4_add_cost_field' into development 2016-06-26 08:26:33 +02:00
Joachim Lusiardi f2623cf6e9 bugfix for creating first pitstop of a vehicle 2016-06-26 08:25:53 +02:00
Joachim Lusiardi 1072142a1b adds consumables to database
this adds a new consumables entity to the database and
the possibility in the admin page to add these.
2016-06-26 08:19:28 +02:00
Joachim Lusiardi 56757f29a1 Merge branch 'issue_3_last_stop_editable' into 'development'
Issue 3 last stop editable



See merge request !15
2016-06-24 07:17:00 +02:00
Joachim Lusiardi 9d41c39ae7 adds edit feature for last pitstop
Adds the possibility to edit the last pitstop in a dedicated
form.
2016-06-24 07:15:24 +02:00
Joachim Lusiardi 6f7716abc6 adds delete feature for last pitstop
Adds the possibility to delete the latest pitstop after a
confirmation page.
2016-06-22 22:48:27 +02:00
Joachim Lusiardi 6d26ceeae8 Merge branch 'issue_4_add_cost_field' into 'development'
adds tracking of costs

This commit adds a new field to each pitstop for tracking of
costs.
This includes:
* a new DB column
* a new field in the create pitstop form
* a new column in the pitstop overview
* new stats values related to costs
* 2 new graphs

See merge request !14
2016-06-22 21:01:46 +02:00
Joachim Lusiardi 5b69a82e05 adds tracking of costs
This commit adds a new field to each pitstop for tracking of
costs.
This includes:
* a new DB column
* a new field in the create pitstop form
* a new column in the pitstop overview
* new stats values related to costs
* 2 new graphs
2016-06-21 21:06:41 +02:00
Joachim Lusiardi f82cf425fd Merge branch 'development' into 'master'
Merge preparation for Cathrine



See merge request !13
2016-05-28 20:47:26 +02:00
Joachim Lusiardi 936fbf30f6 worked on tests 2016-05-28 18:59:23 +02:00
Joachim Lusiardi 7696fb870c Merge branch 'docker_compose' into 'development'
Docker compose



See merge request !12
2016-05-28 17:13:22 +02:00
Joachim Lusiardi 3f8f30f5d1 improved tests
test code waits for selenium to be available
screenshots get named more precisely
2016-05-27 11:33:15 +02:00
Joachim Lusiardi cc414092a3 Adds first steps towards auto tests 2016-05-27 03:24:44 +02:00
Joachim Lusiardi 63ff5845e2 added demo config 2016-05-26 13:54:51 +02:00
Joachim Lusiardi 10494a03a9 first version for docker-compose 2016-05-26 12:08:24 +02:00
Joachim Lusiardi 2176579baa Merge branch 'implement_delete_account' into 'development'
Implement delete account



See merge request !11
2016-05-25 09:41:15 +02:00
Joachim Lusiardi da7c40c78b Merge branch 'index_page' into implement_delete_account 2016-05-25 09:40:20 +02:00
Joachim Lusiardi 00579eb542 Merge branch 'index_page' into implement_delete_account 2016-05-24 23:56:12 +02:00
Joachim Lusiardi ef55b4e479 First step 2016-05-24 23:55:58 +02:00
Joachim Lusiardi a6c5abfd88 Merge branch 'index_page' into 'development'
Index page



See merge request !10
2016-05-24 07:02:33 +02:00
Joachim Lusiardi 293ff50809 Some more UI changes 2016-05-24 06:49:39 +02:00
Joachim Lusiardi 873a28aa28 ui redesign 2016-05-22 11:47:46 +02:00
Joachim Lusiardi 1c4d73da43 Adds start page for unauthed visitors 2016-05-18 08:19:11 +02:00
Joachim Lusiardi 7eef2b6cee Adds start page for unauthed visitors 2016-05-18 08:17:53 +02:00
Joachim Lusiardi 227dc79e6b Merge branch 'issue_1' into 'development'
Handle vehicle name uniqueness

Vehicle names now must be only unique per owner, not globally.
Also errors are displayed to the forms on creation and edit of
vehicles.

See merge request !9
2016-05-16 18:49:39 +02:00
Joachim Lusiardi 19b1e3b7ae Handle vehicle name uniqueness
Vehicle names now must be only unique per owner, not globally.
Also errors are displayed to the forms on creation and edit of
vehicles.
2016-05-16 18:46:24 +02:00
Joachim Lusiardi c5305caf36 switched to configurable db 2016-05-05 15:06:29 +02:00
Joachim Lusiardi e1e5fe7725 Little Improvements
Help text for create pitstops form
More precise error messages for create pitstops form
Fixed bu in statistics page when only one pitstop was present
2016-05-03 22:21:57 +02:00
Joachim Lusiardi 471f1415d9 Removed print 2016-05-02 08:03:24 +02:00
Joachim Lusiardi 99df28a015 Merge branch 'add_charts' into 'master'
Add charts



See merge request !8
2016-05-02 08:02:33 +02:00
Joachim Lusiardi 6b861903af add consumption chart 2016-05-02 07:59:31 +02:00
Joachim Lusiardi 706f30e2e2 switched off drop representation 2016-05-02 07:59:14 +02:00
Joachim Lusiardi cc7d2620e8 Worked on charts 2016-05-02 06:42:43 +02:00
Joachim Lusiardi 161cc16498 continued work ok charts 2016-04-30 06:41:59 +02:00
Joachim Lusiardi ef976299dc Fix PEP8 error 2016-04-28 22:33:14 +02:00
Joachim Lusiardi 60293d6a27 Demo Chart 2016-04-28 07:53:53 +02:00
Joachim Lusiardi 1e5b63d068 Merge branch 'add_app_step_log' into 'master'
fixed variable name



See merge request !7
2016-04-28 07:11:10 +02:00
Joachim Lusiardi 13a5b07df2 fixed variable name 2016-04-28 07:10:44 +02:00
Joachim Lusiardi 662e5b5b63 Merge branch 'add_app_step_log' into 'master'
Add app step log



See merge request !6
2016-04-28 06:44:46 +02:00
Joachim Lusiardi fd206de263 add logging for performed db operations 2016-04-28 06:32:51 +02:00
Joachim Lusiardi e936f1c60f fixed typos in readme 2016-04-27 21:41:47 +02:00
Joachim Lusiardi a7df0aebee Merge branch 'add_vehicles' into 'master'
Add vehicles



See merge request !5
2016-04-27 07:24:59 +02:00
Joachim Lusiardi 3e2d62b28f improved design of the form elements 2016-04-27 07:23:28 +02:00
Joachim Lusiardi ca7166f731 fixed stats 2016-04-25 22:28:35 +02:00
Joachim Lusiardi 34905921d9 fixes display of last pitstops
the display of the last pitstops now is separated by vehicle.
2016-04-25 07:23:01 +02:00
Joachim Lusiardi 038e255b56 can add pitstops to vehicles nows 2016-04-24 14:09:41 +02:00
Joachim Lusiardi 3709eddabb prevents the deletion of vehicles of other users 2016-04-24 12:42:07 +02:00
Joachim Lusiardi c941e1bccf finished modules merge 2016-04-23 23:24:25 +02:00
Joachim Lusiardi 7bc5422687 Merge branch 'create_modules' into add_vehicles
Conflicts:
	app/main.py
2016-04-23 23:22:44 +02:00
Joachim Lusiardi 0d8ae0cd18 introduces modules 2016-04-23 23:08:39 +02:00
Joachim Lusiardi 3ec2e4648d Merge branch 'user_handling' into 'master'
User handling



See merge request !4
2016-04-23 17:35:10 +02:00
Joachim Lusiardi 267c59cbb3 adds create and delete vehicle 2016-04-23 17:11:43 +02:00
Joachim Lusiardi d19b0a5858 adds edit vehicle name 2016-04-23 11:20:39 +02:00
Joachim Lusiardi 729fee51e9 Merge branch 'user_handling' into add_vehicles
Conflicts:
	app/main.py
2016-04-21 21:52:58 +02:00
Joachim Lusiardi 4261a9bb0b reworked git ignore file 2016-04-21 08:12:12 +02:00
Joachim Lusiardi 0303be7945 more user handling features of flask-security activated
Now users can recover their passwords and change them while
logged in.
2016-04-21 08:11:57 +02:00
Joachim Lusiardi 01798e9548 adds admin page
admin users must have role admin (which must currently be set
manually via database)
2016-04-19 22:41:40 +02:00
Joachim Lusiardi 9eb7ec7c8a switched to environment variable for config file lookup 2016-04-19 21:09:15 +02:00
Joachim Lusiardi 344db8d998 add example config file 2016-04-19 08:19:31 +02:00
Joachim Lusiardi 1240a3c64b Users are allowed to register now
This includes sending a welcome email.
2016-04-19 07:39:17 +02:00
Joachim Lusiardi baca41619d first iteration of vehicles 2016-04-19 06:47:10 +02:00
Joachim Lusiardi 2e0fc3b772 Merge branch 'master' into user_handling
Conflicts:
	app/main.py
	app/requirements.txt
2016-04-18 21:43:01 +02:00
Joachim Lusiardi 1d449cb0ab Merge branch 'master' of code.nerd2nerd.org:n0ob/rollerverbrauch 2016-04-18 21:01:31 +02:00
Joachim Lusiardi 4c97bd86db Fixed date vs datetime 2016-04-18 20:58:20 +02:00
Joachim Lusiardi 8057c59329 Merge branch 'wtforms' into 'master'
Wtforms



See merge request !3
2016-04-18 20:35:58 +02:00
Joachim Lusiardi 1998fcb7d8 Merge branch 'master' into wtforms
Conflicts:
	app/main.py
2016-04-18 20:34:39 +02:00
Joachim Lusiardi 01019c22d9 add switch for debugger 2016-04-18 08:13:27 +02:00
Joachim Lusiardi 864b7822ac Changed to WTForms including validation 2016-04-18 07:25:40 +02:00
Joachim Lusiardi 6f218e591f add render_field macro to layout.html 2016-04-17 15:06:54 +02:00
Joachim Lusiardi 8c8803874e First step for WTForms 2016-04-17 12:50:31 +02:00
Joachim Lusiardi 6ed4110ce9 logout only if logged in 2016-04-17 09:59:34 +02:00
Joachim Lusiardi a9b2781ae3 custom login form added 2016-04-17 09:34:11 +02:00
Joachim Lusiardi f6f1d960b7 Add logout button to menu 2016-04-17 09:05:59 +02:00
Joachim Lusiardi bbfa7b9341 First step towards flask security user handling 2016-04-17 08:59:11 +02:00
Joachim Lusiardi 808e0d80d3 Merge branch 'introduce_sqlalchemy' into 'master'
switched to Flask-SQLAlchemy for db handling



See merge request !2
2016-04-16 15:57:56 +02:00
Joachim Lusiardi fc421558b0 switched to Flask-SQLAlchemy for db handling 2016-04-16 15:48:24 +02:00
Joachim Lusiardi c1adf69ca0 Merge branch 'reduce_features' into 'master'
Reduce features



See merge request !1
2016-04-14 19:54:40 +02:00
Joachim Lusiardi 059c86b449 add .gitignore file 2016-04-14 19:53:49 +02:00
Joachim Lusiardi fb521194f7 Reduce services feature and reformatting 2016-04-14 07:32:06 +02:00
Joachim Lusiardi ef02cc8ab3 Add basic build and run information 2016-04-14 07:02:28 +02:00
Joachim Lusiardi 8f99b798bb reworked docker file for faster build times 2016-04-13 08:30:13 +02:00
119 changed files with 4443 additions and 644 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sh text eol=lf

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea/
tests/results/
**.DS_Store
**.swp
**.pyc
*.sqlite

View File

@ -1,9 +1,14 @@
FROM debian8_python3
FROM python:3
COPY requirements.txt /requirements.txt
RUN mkdir /data; \
pip3 install -r /requirements.txt;
ADD app /app
RUN pip3 install -r /app/requirements.txt; \
mkdir /data
ADD main.py /main.py
ADD config.py /config.py
VOLUME ["/data"]
VOLUME ["/app/config"]
EXPOSE 5000
ENTRYPOINT python3 /app/main.py
ENTRYPOINT python3 /main.py

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# How to build & run
## build
`docker build --tag=$(basename $PWD) .`
## general configuration
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*:
`ALTER TABLE pitstop ADD COLUMN costs DECIMAL(5,2) NOT NULL DEFAULT 0.0 AFTER vehicle_id;`
## run in development
Include the development version of the code as volume, so the app gets
reloaded automatically. The sqlite file will be stored in *tmp* so it
can be inspected with tools like *sqlite3*. The switch *DEBUG* enables
debugging during development.
`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/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`

122
app/__init__.py Normal file
View File

@ -0,0 +1,122 @@
from flask import Flask, make_response
from flask import g
from flask_mail import Mail
from flask_security import Security, SQLAlchemyUserDatastore, user_registered
from flask_sqlalchemy import SQLAlchemy
import os
from config import config
from flask_security.forms import LoginForm
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
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=["500 per second"]
)
@app.errorhandler(429)
def ratelimit_handler(e):
return make_response(
jsonify(error="ratelimit exceeded %s" % e.description)
, 429
)
db = SQLAlchemy(app)
mail = Mail(app)
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(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 == 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)
new_vehicle = Vehicle('default vehicle')
db.session.add(new_vehicle)
user.vehicles.append(new_vehicle)
db.session.commit()
tools.db_log_add(user)
tools.db_log_add(new_vehicle)
def assure_consumable(name, ext_id, unit):
if not Consumable.query.filter(Consumable.ext_id == ext_id).first():
c = Consumable(name, ext_id, unit)
db.session.add(c)
@app.before_first_request
def before_first_request():
db.create_all()
# make sure all consumables from tankerkoenig exist: diesel, e5, e10
assure_consumable('Diesel', 'diesel', 'L')
assure_consumable('Super','e5', 'L')
assure_consumable('Super E10','e10', 'L')
user_datastore.find_or_create_role(name='admin', description='Role for administrators')
user_datastore.find_or_create_role(name='user', description='Role for all users.')
db.session.commit()
@app.before_request
def before_request():
g.data = {}
@app.route('/')
def index():
if current_user.is_authenticated:
return redirect(url_for('get_pit_stops'))
else:
user_count = len(User.query.all())
consumables = Consumable.query.all()
per_consumable = {}
for consumable in consumables:
per_consumable[consumable.id] = {
'name': consumable.name,
'unit': consumable.unit,
'amount': 0
}
vehicles = Vehicle.query.all()
kilometers = 0
for vehicle in vehicles:
stats = tools.VehicleStats(vehicle)
for consumable in stats.consumables:
per_consumable[consumable.id]['amount'] += consumable.overall_amount
kilometers += stats.overall_distance
vehicle_count = len(vehicles)
pitstop_count = len(Pitstop.query.all())
data = {
'users': user_count,
'vehicles': vehicle_count,
'pitstops': pitstop_count,
'kilometers': kilometers,
'consumables': per_consumable
}
return render_template('index.html', login_user_form=LoginForm(), data=data)

View File

@ -0,0 +1,6 @@
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'your-gmail-username'
MAIL_PASSWORD = 'your-gmail-password'

View File

@ -1,92 +0,0 @@
'''
@author: shing19m
'''
from datetime import datetime
import sqlite3
class Db(object):
'''
classdocs
'''
def __init__(self, db_file):
'''
Constructor
'''
self.db = sqlite3.connect(db_file)
def __del__(self):
self.db.close()
def getAllPitStops(self):
return self._perform_query('select * from pitstops order by id asc')
def getAllServices(self):
return self._perform_query('select * from services')
def getLastPitStop(self):
pitstops = self._perform_query('select * from pitstops order by id desc limit 1')
if len(pitstops) == 0:
return {'date': datetime.strftime(datetime.now(), '%Y-%m-%d'), 'odometer': 0, 'litres': 0}
return pitstops[0]
def get_service_warning_info(self):
info = self._perform_query('select (odometer_planned - (select odometer from pitstops order by id desc limit 1)) km_left, tasks from services where date is null order by odometer_planned asc limit 1;')
if len(info) == 0:
return None
return info[0]
def get_next_undone_service(self):
services = self._perform_query('select * from services where date is null limit 1')
if len(services) == 0:
return None
return services[0]
def get_salt_for_user(self, user):
salt = self._perform_query_param('select salt from users where name = ?', [user])
if len(salt) == 0:
return None
return salt[0]['salt']
def check_password_for_user(self, user, password):
user = self._perform_query_param('select * from users where name = ? and password = ?', [user, password])
if len(user) == 0:
return False
return True
def _perform_query_param(self, query, data):
cursor = self.db.execute(query, data)
names = list(map(lambda x: x[0], cursor.description))
result = []
for row in cursor.fetchall():
row_result = {}
for index in range(0, len(names)):
row_result[names[index]] = row[index]
result.append(row_result)
return result
def _perform_query(self, query):
cursor = self.db.execute(query)
names = list(map(lambda x: x[0], cursor.description))
result = []
for row in cursor.fetchall():
row_result = {}
for index in range(0, len(names)):
row_result[names[index]] = row[index]
result.append(row_result)
return result
def addPitStop(self, date, odometer, litres):
self.db.execute('insert into pitstops (date, odometer, litres) values (?, ?, ?)', [date, odometer, litres])
self.db.commit()
def init_db(self, resource):
with resource as f:
sql_commands = f.read()
self.db.cursor().executescript(sql_commands)
self.db.commit()

240
app/entities.py Normal file
View File

@ -0,0 +1,240 @@
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")),
)
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")
),
)
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))
def __str__(self):
return self.name
def __hash__(self):
return hash(self.name)
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())
home_lat = db.Column(db.Numeric(8, 5), default=0)
home_long = db.Column(db.Numeric(8, 5), default=0)
home_zoom = db.Column(db.Integer(), default=0)
vehicles = db.relationship("Vehicle")
roles = db.relationship(
"Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic")
)
favourite_filling_stations = db.relationship(
"FillingStation", secondary=users_fillingstations
)
def __repr__(self):
return '<User id="%r" email="%r" ' % (self.id, self.email)
class Vehicle(db.Model):
"""
Entity to represent a vehicle.
Attributes:
* name of the vehilce
* the id of the owner
* list of pitstops
* list of possible consumables
"""
id = db.Column(db.Integer, primary_key=True)
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"))
name = db.Column(db.String(255))
pitstops = db.relationship("Pitstop", 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)
is_active = db.Column(db.Boolean(), default=True)
# 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
def __repr__(self):
return '<Vehicle id="%r" owner_id="%r" name="%r" />' % (
self.id,
self.owner_id,
self.name,
)
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"))
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, amount, date, costs, consumable_id):
self.odometer = odometer
self.amount = amount
self.date = date
self.costs = costs
self.consumable_id = consumable_id
def __repr__(self):
return '<Pitstop odometer="%r" amount="%r" date="%r" vehicle_id="%r" consumable_id="%r">' % (
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):
"""
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)
ext_id = db.Column(db.String(255))
unit = db.Column(db.String(255))
vehicles = db.relationship("Vehicle", secondary=vehicles_consumables)
def __init__(self, name, ext_id, unit):
self.name = name
self.ext_id = ext_id
self.unit = unit
def __repr__(self):
return '<Consumable name="%s" unit="%s" />' % (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 (
'<Service odometer="%r" date="%r" vehicle_id="%r" costs="%r" description="%r">'
% (self.odometer, self.date, self.vehicle_id, self.costs, self.description)
)
class FillingStation(db.Model):
int_id = db.Column(db.Integer, primary_key=True)
id = db.Column(db.String(40), unique=True, nullable=False)
name = db.Column(db.Text(), nullable=False)
street = db.Column(db.Text(), nullable=False)
place = db.Column(db.Text(), nullable=False)
houseNumber = db.Column(db.Text())
postCode = db.Column(db.Integer(), nullable=False)
brand = db.Column(db.Text(), nullable=False)
lat = db.Column(db.Numeric(8, 5), nullable=False)
lng = db.Column(db.Numeric(8, 5), nullable=False)
last_update = db.Column(db.DateTime)
diesel = db.Column(db.Numeric(10, 3), default=0)
e5 = db.Column(db.Numeric(10, 3), default=0)
e10 = db.Column(db.Numeric(10, 3), default=0)
open = db.Column(db.Boolean())
def as_dict(self):
res = {}
for c in self.__table__.columns:
val = getattr(self, c.name)
import decimal
if isinstance(val, decimal.Decimal):
val = float(val)
val = str(val)
res[c.name] = val
return res

28
app/filters.py Normal file
View File

@ -0,0 +1,28 @@
from app import app
import hashlib
import markdown
@app.template_filter('none_filter')
def none_filter(value):
if value is None:
return ''
else:
return value
@app.template_filter('md5')
def md5_filter(value):
m = hashlib.md5()
m.update(str(value).encode('UTF-8'))
return m.hexdigest()
@app.template_filter('str')
def str_filter(value):
return str(value)
@app.template_filter('markdown')
def md(value):
return markdown.markdown(value)

7
app/forms/__init__.py Normal file
View File

@ -0,0 +1,7 @@
from .misc import *
from .pitstop import *
from .checks import *
from .consumable import *
from .service import *
from .vehicle import *
from .regular_cost import *

174
app/forms/checks.py Normal file
View File

@ -0,0 +1,174 @@
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.
: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 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):
"""
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 €.")

26
app/forms/consumable.py Normal file
View File

@ -0,0 +1,26 @@
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)])
ext_id = SelectField('Tankerkönig ID', coerce=int)
unit = StringField('Unit', validators=[Length(1, 255)])
submit = SubmitField(label='Do it!')
class EditConsumableForm(FlaskForm):
name = StringField('Name', validators=[Length(1, 255)])
ext_id = SelectField('Tankerkönig ID', coerce=int)
unit = StringField('Unit', validators=[Length(1, 255)])
submit = SubmitField(label='Do it!')
class DeletConsumableForm(FlaskForm):
submit = SubmitField(label='Do it!')

6
app/forms/misc.py Normal file
View File

@ -0,0 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import SubmitField
class DeleteAccountForm(FlaskForm):
submit = SubmitField(label='Really delete my account!')

93
app/forms/pitstop.py Normal file
View File

@ -0,0 +1,93 @@
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')
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=[costs_check])
submit = SubmitField(label='Update it!')
same_odometer_allowed = True
pitstops = []
def set_pitstops(self, pitstops):
self.pitstops = pitstops
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
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')
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!')
same_odometer_allowed = True
pitstops = []
def set_pitstops(self, pitstops):
self.pitstops = pitstops
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
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
elif len(self.pitstops) > 0 and 'amount' in self.pitstops[-1].__dict__:
self.litres.default = self.pitstops[-1].amount
else:
self.litres.default = 0
if self.costs.data:
self.costs.default = self.costs.data
elif len(self.pitstops) > 0:
self.costs.default = self.pitstops[-1].costs
else:
self.costs.default = 0
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

86
app/forms/regular_cost.py Normal file
View File

@ -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

75
app/forms/service.py Normal file
View File

@ -0,0 +1,75 @@
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')
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!')
pitstops = []
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
elif len(self.pitstops) > 0:
self.odometer.default = self.pitstops[-1].odometer
else:
self.odometer.default = 0
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
class DeleteServiceForm(FlaskForm):
submit = SubmitField(label='Really delete this service!')
class EditServiceForm(FlaskForm):
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!')
same_odometer_allowed = True
pitstops = []
def set_pitstops(self, pitstops):
self.pitstops = pitstops
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.costs.data:
self.costs.default = self.costs.data
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

25
app/forms/vehicle.py Normal file
View File

@ -0,0 +1,25 @@
from flask_wtf import FlaskForm
from wtforms import (
StringField,
SubmitField,
SelectField,
SelectMultipleField,
BooleanField,
)
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=[])
is_active = BooleanField("Is active")
submit = SubmitField(label="Do it!")
class DeleteVehicleForm(FlaskForm):
submit = SubmitField(label="Do it!")

View File

@ -1,186 +0,0 @@
from datetime import datetime
from flask import Flask
from flask import render_template, make_response
from flask import request, redirect, g
from flask import url_for
from flask import Response
import hashlib
import os.path
import time
from functools import wraps
import db
#from db import Db
app = Flask(__name__)
DATABASE = '/data/rollerverbrauch.db'
DEBUG = True
SECRET_KEY = 'development key'
app.config.from_object(__name__)
def check_auth(username, password):
salt = g.db2.get_salt_for_user(username)
if salt == None:
return False
m = hashlib.sha256(password.encode('utf-8'))
m = hashlib.sha256((m.hexdigest()+salt).encode('utf-8'))
digest = m.hexdigest()
ok = g.db2.check_password_for_user(username, digest)
if not ok:
app.logger.error("digest: " + digest)
return ok
def authenticate():
resp = make_response(render_template('login_required.html'), 401)
resp.headers['WWW-Authenticate'] = 'Basic realm="Login Required"'
return resp
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
@app.before_request
def before_request():
g.db2 = db.Db(app.config['DATABASE'])
g.data = {}
add_service_warning(g.data)
@app.teardown_request
def teardown_request(exception):
pass
@app.route('/')
@requires_auth
def index():
return redirect(url_for('get_pit_stops'))
@app.route('/services')
@requires_auth
def get_services():
data = g.db2.getAllServices()
data.reverse()
g.data['services'] = data
return render_template('services.html', data=g.data)
@app.route('/pitstops', methods=['POST'])
@requires_auth
def create_pit_stop():
last_pitstop = g.db2.getLastPitStop()
errorMsg = {}
date = request.form['date']
try:
date = datetime.strptime(date, '%Y-%m-%d').strftime('%Y-%m-%d')
except ValueError:
errorMsg['date'] = 'invalid date, only YYYY-MM-DD is allowed'
date = request.form['date']
odometer = request.form['odometer']
try:
odometer = int(odometer)
except ValueError:
errorMsg['odometer'] = 'Illegal Value, only Integers allowed'
odometer = None
if odometer is not None and odometer <= last_pitstop['odometer']:
errorMsg['odometer'] = 'Illegal Value, new Value must be bigger as given value'
odometer = request.form['odometer']
if odometer is None:
odometer = request.form['odometer']
litres = request.form['litres']
try:
litres = float(litres)
except ValueError:
errorMsg['litres'] = 'Illegal Value, only floating point allowed'
litres = None
if litres is not None and litres <= 0:
errorMsg['litres'] = 'Litres must not be 0'
litres = request.form['litres']
if litres is None:
litres = request.form['litres']
# error checking here
if len(errorMsg) > 0:
data = {'last': {'date': date, 'odometer': odometer, 'litres': litres}, 'error': errorMsg}
return render_template('newPitStopForm.html', data=data)
g.db2.addPitStop(date, odometer, litres)
return redirect(url_for('get_pit_stops'))
@app.route('/pitstops/createForm', methods=['GET'])
@requires_auth
def create_pit_stop_form():
values = g.db2.getLastPitStop()
values['date'] = time.strftime("%Y-%m-%d")
g.data['last'] = values
g.data['error'] = None
return render_template('newPitStopForm.html', data=g.data)
def add_service_warning(data):
service_info = g.db2.get_service_warning_info()
data['service_info'] = service_info
@app.route('/pitstops', methods=['GET'])
@requires_auth
def get_pit_stops():
data = prepare_pit_stops(g.db2.getAllPitStops())
g.data['pitstops'] = data
return render_template('pitstops.html', data=g.data)
@app.route('/manual', methods=['GET'])
@requires_auth
def get_manual():
return render_template('manual.html', data=g.data)
@app.route('/statistics', methods=['GET'])
@requires_auth
def get_statistics():
pitstops = g.db2.getAllPitStops()
count = len(pitstops)
distance = 0
sumLitres = 0
averageDistance = 0
averageLitresFuelled = 0
averageLitresUsed = 0
if count > 0:
sumLitres = 0
for pitstop in pitstops:
sumLitres += pitstop['litres']
averageLitresFuelled = sumLitres/count
if count > 1:
distance = pitstops[-1]['odometer'] - pitstops[0]['odometer']
averageDistance = distance/(count - 1)
averageLitresUsed = 100 * (sumLitres-pitstops[0]['litres'])/distance
g.data['distance'] = distance
g.data['count'] = count
g.data['litres'] = sumLitres
g.data['averageDistance'] = averageDistance
g.data['averageListresFuelled'] = averageLitresFuelled
g.data['averageListresUsed'] = averageLitresUsed
return render_template('statistics.html', data=g.data)
def prepare_pit_stops(pitstops):
for index in range(1, len(pitstops)):
last = pitstops[index - 1]
curr = pitstops[index]
curr['distance'] = curr['odometer'] - last['odometer']
curr['average'] = 100 * curr['litres']/curr['distance']
last_date = datetime.strptime(last['date'], '%Y-%m-%d')
curr_date = datetime.strptime(curr['date'], '%Y-%m-%d')
curr['days'] = (curr_date - last_date).days
pitstops.reverse()
return pitstops
if __name__ == '__main__':
if not os.path.isfile(DATABASE) or os.stat(DATABASE).st_size == 0:
db = db.Db(app.config['DATABASE'])
db.init_db(app.open_resource('schema.sql', mode='r'))
app.run(debug=True, host='0.0.0.0')

View File

@ -1,2 +0,0 @@
#db-sqlite3
Flask

7
app/routes/__init__.py Normal file
View File

@ -0,0 +1,7 @@
from .account import *
from .admin import *
from .misc import *
from .pitstop import *
from .service import *
from .filling_stations import *
from .regular_cost import *

165
app/routes/account.py Normal file
View File

@ -0,0 +1,165 @@
from flask import url_for, redirect, render_template, request, jsonify
from flask_security import login_required
from flask_security.core import current_user
from sqlalchemy.exc import IntegrityError
import json
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():
stations = [x.as_dict() for x in current_user.favourite_filling_stations]
for station in stations:
station["state"] = "favourite"
return render_template(
"account.html",
map_pos=(current_user.home_lat, current_user.home_long, current_user.home_zoom),
fs=json.dumps(stations),
)
@app.route("/account/vehicle/edit/<int:vid>", 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.is_active.data is not None:
form.is_active.default = form.is_active.data
if form.validate_on_submit():
vehicle.name = form.name.data
vehicle.is_active = form.is_active.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.is_active.default = vehicle.is_active
form.process()
return render_template("editVehicleForm.html", form=form, vehicle=vehicle)
@app.route("/account/vehicle/delete/<int:vid>", 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)
@app.route("/account/home", methods=["GET"])
@login_required
def get_users_home():
return jsonify(
{
"lat": float(current_user.home_lat),
"long": float(current_user.home_long),
"zoom": current_user.home_zoom,
}
)
@app.route("/account/home", methods=["POST"])
@login_required
def set_users_home():
current_user.home_lat = request.json["lat"]
current_user.home_long = request.json["long"]
current_user.home_zoom = request.json["zoom"]
db.session.commit()
return jsonify({})

107
app/routes/admin.py Normal file
View File

@ -0,0 +1,107 @@
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()
choices = [(0, ''), (1, 'diesel'), (2, 'e5'), (3, 'e10')]
form.ext_id.choices = choices
# 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, choices[form.ext_id.data][1], 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/<int:cid>', 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/<int:cid>', 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()
choices = [(0, ''), (1, 'diesel'), (2, 'e5'), (3, 'e10')]
form.ext_id.choices = choices
form.name.default = consumable.name
form.unit.default = consumable.unit
form.ext_id.default = 3
for c in choices:
if c[1] == consumable.ext_id:
form.ext_id.default = c[0]
# 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.ext_id.data is not None:
form.ext_id.default = form.ext_id.data
if form.validate_on_submit():
consumable.name = form.name.data
consumable.unit = form.unit.data
consumable.ext_id = choices[form.ext_id.data][1]
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)

View File

@ -0,0 +1,62 @@
from flask import request, jsonify
from flask_security import login_required
from flask_security.core import current_user
import requests
from ..entities import FillingStation
from .. import app, db, limiter
@app.route('/filling_stations/favourites/toggle/<fsid>')
def add_favourite_filling_stations(fsid):
favourite_ids = {x.id: x for x in current_user.favourite_filling_stations}
if fsid in favourite_ids:
current_user.favourite_filling_stations.remove(favourite_ids[fsid])
state = 'normal'
else:
fs = FillingStation.query.filter(FillingStation.id == fsid).first()
current_user.favourite_filling_stations.append(fs)
state = 'favourite'
db.session.commit()
return jsonify({'state': state})
@app.route('/filling_stations', methods=['GET'])
@login_required
@limiter.limit('1 per second')
def query_filling_stations():
api_key = app.config['TANKERKOENIG_API_KEY']
latitude = request.args.get('latitude')
longitude = request.args.get('longitude')
radius = request.args.get('radius', default=1.5)
gas_type = request.args.get('type', default='all')
sort = request.args.get('sort', default='dist')
url = 'https://creativecommons.tankerkoenig.de/json/list.php'
params = {
'lat': latitude, 'lng': longitude, 'rad': radius, 'apikey': api_key, 'type': gas_type, 'sort': sort
}
response = requests.get(url, params=params)
data = response.json()
for station in data['stations']:
fs = FillingStation.query.filter(FillingStation.id == station['id']).first()
if not fs:
fs = FillingStation()
fs.id = station['id']
fs.brand = station['brand']
fs.lat = station['lat']
fs.lng = station['lng']
fs.name = station['name']
fs.street = station['street']
fs.place = station['place']
fs.houseNumber = station['houseNumber']
fs.postCode = station['postCode']
db.session.add(fs)
if fs in current_user.favourite_filling_stations:
station['state'] = 'favourite'
else:
station['state'] = 'normal'
db.session.commit()
return jsonify(data)

22
app/routes/misc.py Normal file
View File

@ -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 = []
def key(v):
return (not v.is_active, v.name)
vehicles = sorted(current_user.vehicles, key=key)
for vehicle in vehicles:
stats.append(VehicleStats(vehicle))
return render_template("statistics.html", data=stats)

314
app/routes/pitstop.py Normal file
View File

@ -0,0 +1,314 @@
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
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,
RegularCostInstance,
calculate_regular_cost_instances,
get_users_active_vehicle,
)
from .. import app, db
@app.route("/pitstops/vehicle/select", methods=["GET", "POST"])
@login_required
def select_vehicle_for_new_pitstop():
active_vehicles = get_users_active_vehicle(current_user)
if len(active_vehicles) == 1:
return redirect(
url_for(
"select_consumable_for_new_pitstop", vid=active_vehicles[0].id
)
)
form = SelectVehicleForm()
form.vehicle.choices = [
(g.id, g.name) for g in active_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/<int:vid>/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/<int:vid>/consumable/<int:cid>/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_pitstops(data)
form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (
data[-1].consumable.id != cid
)
else:
form.set_pitstops([])
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/<int:pid>", 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/<int:pid>", 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"))
form = EditPitstopForm()
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
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
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.preinit_with_data()
form.process()
return render_template(
"editPitStopForm.html",
form=form,
vehicle=vehicle,
messages=form.get_hint_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)
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,
"regulars": vehicle.regulars,
}
user["vehicles"].append(v)
return render_template("pitstops.html", user=user)
@app.route("/pitstops/plan/vehicle/select", methods=["GET", "POST"])
@login_required
def select_vehicle_for_plan_pitstop():
if len(current_user.vehicles) == 1:
return redirect(
url_for(
"select_consumable_for_plan_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_plan_pitstop", vid=form.vehicle.data)
)
return render_template("selectVehicle.html", form=form)
@app.route(
"/pitstops/plan/vehicle/<int:vid>/consumable/select", methods=["GET", "POST"]
)
@login_required
def select_consumable_for_plan_pitstop(vid):
vehicle = Vehicle.query.get(vid)
if vehicle is None or vehicle not in current_user.vehicles:
return redirect(url_for("select_consumable_for_plan_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("plan_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("plan_pit_stop_form", vid=vid, cid=form.consumable.data)
)
return render_template(
"selectConsumableForVehicle.html", vehicle=vehicle, form=form
)
@app.route(
"/pitstops/plan/vehicle/<int:vid>/consumable/<int:cid>", methods=["GET", "POST"]
)
@login_required
def plan_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))
update_filling_station_prices(
[x.id for x in current_user.favourite_filling_stations]
)
offers = []
for fs in current_user.favourite_filling_stations:
offers.append(
(
fs,
getattr(fs, consumable.ext_id),
)
)
return render_template(
"planPitStopForm.html", vehicle=vehicle, consumable=consumable, offers=offers
)

162
app/routes/regular_cost.py Normal file
View File

@ -0,0 +1,162 @@
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,
get_users_active_vehicle,
)
from .. import app, db
@app.route("/regular_costs/delete/<int:pid>", 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():
active_vehicles = get_users_active_vehicle(current_user)
if len(active_vehicles) == 1:
return redirect(
url_for("create_regular_cost_for_vehicle", vid=active_vehicles[0].id)
)
form = SelectVehicleForm()
form.vehicle.choices = [(g.id, g.name) for g in active_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/<int:vid>/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/<int:pid>", 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/<int:pid>", 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,
)

137
app/routes/service.py Normal file
View File

@ -0,0 +1,137 @@
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,
get_users_active_vehicle,
)
from .. import app, db
@app.route("/service/vehicle/<int:vid>/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_pitstops(data)
form.same_odometer_allowed = type(data[-1]) != Service
else:
form.set_pitstops([])
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/<int:sid>", 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/<int:sid>", 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"))
data = get_event_line_for_vehicle(vehicle)
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
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.preinit_with_data()
form.process()
return render_template(
"editServiceForm.html",
form=form,
vehicle=vehicle,
messages=form.get_hint_messages(),
)
@app.route("/service/vehicle/select", methods=["GET", "POST"])
@login_required
def select_vehicle_for_new_service():
active_vehicles = get_users_active_vehicle(current_user)
if len(active_vehicles) == 1:
return redirect(
url_for("create_service_for_vehicle", vid=active_vehicles[0].id)
)
form = SelectVehicleForm()
form.vehicle.choices = [(g.id, g.name) for g in active_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)

View File

@ -1,25 +0,0 @@
drop table if exists pitstops;
create table pitstops (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`date` TEXT NOT NULL,
`odometer` INTEGER NOT NULL,
`litres` REAL NOT NULL
);
drop table if exists services;
CREATE TABLE `services` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`date` TEXT,
`odometer_planned` INTEGER NOT NULL,
`odometer_done` INTEGER,
`tasks` TEXT NOT NULL
);
drop table if exists users;
create table `users` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`salt` TEXT NOT NULL,
`password` TEXT NOT NULL
);
insert into users (name, salt, password) values ('shing19m', 'pL85Kl2U', '207357fdbf6f379c53bb5ab7fa0bc8c0072ae743973a510f551db7b5c90049b7');

126
app/static/css/main.css Normal file
View File

@ -0,0 +1,126 @@
.markdown > h1 {
font-size: 14px;
font-weight: bold;
}
.markdown {
text-align: left
}
body {
padding-top: 50px;
}
.starter-template {
padding-top: 0px;
// padding-bottom: 60px;
text-align: center;
}
th {
text-align: center;
padding-left: 10px;
padding-right: 10px;
}
td {
text-align: right;
padding-left: 10px;
padding-right: 10px;
}
.error {
color: #a94442;
}
.nav-pills > li > a {
border: 1px solid;
}
h1 {
margin-top: 0px;
}
h2 {
margin-top: 0px;
}
h3 {
margin-top: 0px;
text-align: center;
}
h3:before{
content:"― ";
}
h3:after{
content:" ―";
}
.tab-content > .active {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 1px;
padding-top: 15px;
}
.panel-body > p, .panel-body > ul {
text-align: left;
}
.alert {
margin-bottom:0px;
}
.topspace {
margin-top: 5px;
}
// for small devices
@media only screen
and (min-device-width : 320px)
and (max-device-width : 568px) {
h3:before{
content:"";
}
h3:after{
content:"";
}
#charts_tabs {
display:none;
}
#charts_tabs-content {
display:none;
}
}
.filling_station_info {
margin: 5px;
border: 1px solid;
}
.filling_station_info img{
margin-top: 6px;
height:48px;
}
/*
* styling for sortable tables
*/
th.header {
background-image: url(../img/updown.gif);
background-repeat: no-repeat;
background-position: center right;
}
th.headerSortUp {
background-image: url(../img/down.gif);
}
th.headerSortDown {
background-image: url(../img/up.gif);
}
.filling_station_closed {
text-decoration: line-through;
}

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
app/static/img/down.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

View File

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 980 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/static/img/up.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

BIN
app/static/img/updown.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,184 @@
// initially display germany
var lat = 50.75653081787912,
lon = 9.262980794432847,
zoom = 5;
var map;
var filling_stations = {};
var filling_station_markers;
query_location = function(updater) {
if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
lat = position.coords.latitude;
lon = position.coords.longitude;
zoom = 11;
if(updater){
updater(lat, lon);
}
});
}
}
update_map = function() {
var lonLat = new OpenLayers.LonLat( lon, lat )
.transform(
new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
map.getProjectionObject() // to Spherical Mercator Projection
);
map.setCenter (lonLat, zoom);
}
load_filling_stations = function() {
var url = '/filling_stations?latitude=' + lat + '&longitude='+ lon + '&type=all&radius=5&sort=dist';
$.ajax({
type: 'GET',
url: url,
success: function(data) {
data.stations.forEach(function(station) {
if (!(station.id in filling_stations)) {
filling_stations[station.id] = station;
filling_stations[station.id].marker = false;
}
});
update_filling_station_markers();
}
});
}
clicked_on_filling_station_marker = function(station, marker) {
return function(data) {
$.ajax({
type: 'GET',
url: '/filling_stations/favourites/toggle/'+station.id,
dataType: 'json',
timeout: 1000,
success: function(data) {
if (data.state == 'favourite') {
marker.setUrl('/static/img/filling_station_favourite_marker.png');
} else {
marker.setUrl('/static/img/filling_station_marker.png');
}
},
contentType : 'application/json'
});
}
}
display_station_information = function(station) {
return function(event) {
var info = $('#station_info');
info.empty();
info.addClass('filling_station_info');
var cell1 = $('<div>', {'class':'col-md-8'})
var cell2 = $('<div>', {'class':'col-md-4'})
var img = $('<img>', {'src': '/static/logos/'+station.brand.toLowerCase()+'.png'});
// hide if the brand icon loads with error
img.error(function(){
$(this).hide();
});
var name = $('<div>').text(station.name);
var street_number = $('<div>').text(station.street + ' ' + station.houseNumber);
var postcode_place = $('<div>').text(station.postCode + ' ' + station.place);
info.append(cell1
.append(name)
.append(street_number)
.append(postcode_place))
.append(cell2
.append(img));
cell2.height(cell1.height());
};
}
update_filling_station_markers = function() {
for(id in filling_stations) {
var station = filling_stations[id];
if(!station.marker) {
var lonLat = new OpenLayers.LonLat(station.lng, station.lat)
.transform(new OpenLayers.Projection('EPSG:4326'), map.getProjectionObject());
if (station.state == 'favourite') {
var icon = new OpenLayers.Icon('/static/img/filling_station_favourite_marker.png');
} else {
var icon = new OpenLayers.Icon('/static/img/filling_station_marker.png');
}
var marker = new OpenLayers.Marker(lonLat, icon);
marker.events.register('click', marker, clicked_on_filling_station_marker(station, marker));
marker.events.register('mouseover', null, display_station_information(station));
filling_station_markers.addMarker(marker);
station.marker = true;
}
}
}
activate_map = function(map_div_id, button_ids, home_lat, home_long, home_zoom, init_stations) {
// resize to reasonable height
$('#' + map_div_id).css('height',0.75*($('#' + map_div_id).css('width')));
// init map
map = new OpenLayers.Map(map_div_id);
map.addLayer(new OpenLayers.Layer.OSM());
map.events.register('moveend', null, function(e){
var p = e.object.center.clone();
var p = p.transform(map.getProjectionObject(), 'EPSG:4326');
lon = p.lon;
lat = p.lat;
zoom = e.object.zoom;
});
filling_station_markers = new OpenLayers.Layer.Markers('Filling Stations');
map.addLayer(filling_station_markers);
// handle initial / favourite stations
filling_stations = init_stations;
update_filling_station_markers();
update_map();
if ((home_lat == 0) && (home_long == 0)) {
query_location(update_map);
} else {
lat = home_lat;
lon = home_long;
zoom = home_zoom;
update_map();
}
// get button
$('#'+button_ids[0]).click(function(e){
load_filling_stations();
});
// set home button
$('#'+button_ids[1]).click(function(e){
$.ajax({
type: 'POST',
url: '/account/home',
data: JSON.stringify({'long': lon, 'lat': lat, 'zoom': zoom}),
dataType: 'json',
timeout: 1000,
contentType : 'application/json'
});
});
// go home button
$('#'+button_ids[2]).click(function(e){
$.ajax({
type: 'GET',
url: '/account/home',
dataType: 'json',
timeout: 1000,
success: function(data) {
lat = data.lat;
lon = data.long;
zoom = data.zoom;
update_map();
},
contentType : 'application/json'
});
});
}

File diff suppressed because one or more lines are too long

80
app/static/js/main.js Normal file
View File

@ -0,0 +1,80 @@
function createChart(id, data, unit) {
return AmCharts.makeChart(id, {
"type": "serial",
//"theme": "chalk",
//"marginRight": 40,
//"marginLeft": 40,
//"autoMarginOffset": 20,
"mouseWheelZoomEnabled":true,
"dataDateFormat": "YYYY-MM-DD",
"valueAxes": [{
"id": "v1",
"axisAlpha": 0,
"position": "left",
"ignoreAxisWidth":true,
"title": unit
}],
"balloon": {
"borderThickness": 1,
"shadowAlpha": 10,
},
"graphs": [{
"id": "g1",
"balloon":{
"drop":false,
"adjustBorderColor":false,
"color":"#ffffff",
},
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#000000",
"bulletSize": 5,
"hideBulletsCount": 50,
"lineThickness": 2,
// "title": unit,
//"useLineColorForBulletBorder": true,
"valueField": "value",
"balloonText": "<span style='font-size:18px;'>[[value]] "+unit+"</span>"
}],
"chartScrollbar": {
"graph": "g1",
"oppositeAxis":false,
"offset":30,
"scrollbarHeight": 80,
"backgroundAlpha": 0,
"selectedBackgroundAlpha": 0.1,
"selectedBackgroundColor": "#888888",
"graphFillAlpha": 0,
"graphLineAlpha": 0.5,
"selectedGraphFillAlpha": 0,
"selectedGraphLineAlpha": 1,
"autoGridCount":true,
"color":"#AAAAAA"
},
"chartCursor": {
"pan": true,
"valueLineEnabled": true,
"valueLineBalloonEnabled": true,
"cursorAlpha":1,
"cursorColor":"#258cbb",
"limitToGraph":"g1",
"valueLineAlpha":0.2,
"valueZoomable":true
},
"valueScrollbar":{
"oppositeAxis":false,
"offset":50,
"scrollbarHeight":10
},
"categoryField": "date",
"categoryAxis": {
"parseDates": true,
"dashLength": 1,
// "minorGridEnabled": true
},
"export": {
"enabled": true
},
"dataProvider": data
});
}

BIN
app/static/logos/agip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
app/static/logos/aral.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/static/logos/avia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/static/logos/bft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/static/logos/esso.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
app/static/logos/hem.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
app/static/logos/jet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
app/static/logos/omv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
app/static/logos/shell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
app/static/logos/total.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,30 +0,0 @@
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
th {
text-align: center;
padding-left: 10px;
padding-right: 10px;
}
td {
text-align: right;
padding-left: 10px;
padding-right: 10px;
}
.error {
color: #a94442;
}
.pitstop {
}
.nav-pills > li > a {
border: 1px solid;
}

View File

@ -1 +0,0 @@

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}

103
app/templates/account.html Normal file
View File

@ -0,0 +1,103 @@
{% extends "layout.html" %}
{% block body %}
<h3>Account management for {{current_user.email}}</h3>
<div class="panel panel-default">
<div class="panel-heading">Password</div>
<div class="panel-body">
<a href="{{ url_for('security.change_password') }}" id="change_password" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change
</a>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Vehicles</div>
<div class="panel-body">
<a href="{{ url_for('create_vehicle') }}" id="create_vehicle" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a>
</div>
<table class="table table-striped table-bordered">
<tbody>
<tr>
<th>
Vehicle
</th>
<th>
Info
</th>
<th>
Actions
</th>
</tr>
{% for vehicle in current_user.vehicles %}
<tr>
<td>
{{ vehicle.name }}<br />
{% if not vehicle.is_active %}
(inactive)
{% endif %}
</td>
<td>
{{ vehicle.pitstops | length }} pitstops<br />
{{ vehicle.services | length }} special expenses<br />
{{ vehicle.consumables | length }} consumables
</td>
<td>
<a href="{{ url_for('create_service_for_vehicle', vid=vehicle.id) }}" id="pitstop_{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> add service
</a>
<a href="{{ url_for('select_consumable_for_new_pitstop', vid=vehicle.id) }}" id="pitstop_{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> add pitstop
</a>
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" id="edit_vehicle_{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
{% if current_user.vehicles | length > 1 %}
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" id="delete_vehicle_{{loop.index}}" class="btn btn-primary btn-warning " role="button">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
{% else %}
&nbsp;
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel panel-default">
<div class="panel-heading">Filling Stations</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6 olMap" style="height: 400px" id="mapdiv"></div>
<div class="col-md-6">
<div class="row">
<div class="btn-group col-md-12" role="group">
<button type="button" class="btn btn-default glyphicon glyphicon-home" id="go_home_button" title="go to home location"/>
<button type="button" class="btn btn-default glyphicon glyphicon-screenshot " id="set_home_button" title="set home location"/>
<button type="button" class="btn btn-default glyphicon glyphicon-download" id="get_button" title="load fuel stations"/>
</div>
</div>
<div id="station_info" class="row">
</div>
</div>
</div>
</div>
<script>
var lat = {{ map_pos[0] or 0 }};
var long = {{ map_pos[1] or 0 }};
var zoom = {{ map_pos[2] or 0 }};
var init_filling_station = JSON.parse({{ fs|tojson }});
activate_map('mapdiv', ['get_button', 'set_home_button', 'go_home_button'], lat, long, zoom, init_filling_station);
</script>
</div>
<div class="panel panel-default">
<div class="panel-heading">Account</div>
<div class="panel-body">
<a href="{{ url_for('delete_account') }}" id="delete_account" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</a>
</div>
</div>
{% endblock %}

60
app/templates/admin.html Normal file
View File

@ -0,0 +1,60 @@
{% extends "layout.html" %}
{% block body %}
<h3>Admin</h3>
<div class="panel panel-default">
<div class="panel-heading">Users</div>
<div class="panel-body">
We have {{ users|length }} users so far:
<ul>
{% for user in users %}
<li>{{user.email}}</li>
{% endfor %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Consumables</div>
<div class="panel-body">
<a href="{{ url_for('create_consumable') }}" id="create_consumable" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a>
</div>
<table class="table table-striped table-bordered">
<tbody>
<tr>
<th>
Name
</th>
<th>
Unit
</th>
<th>
Used by
</th>
<th>
Actions
</th>
</tr>
{% for consumable in consumables %}
<tr>
<td id="name_{{loop.index}}">{{ consumable.name }}</td>
<td id="unit_{{loop.index}}">{{ consumable.unit }}</td>
<td id="count_{{loop.index}}">{{ consumable.vehicles | length }} vehicles</td>
<td>
{% if not consumable.in_use %}
<a href="{{ url_for('delete_consumable', cid=consumable.id) }}" id="delete_consumable{{loop.index}}" class="btn btn-primary btn-warning " role="button">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
{% endif %}
<a href="{{ url_for('edit_consumable', cid=consumable.id) }}" id="edit_consumable{{loop.index}}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Create consumable</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.ext_id) }}
{{ render_field_with_errors(form.unit) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>New Pitstop for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.date) }}
<span id="{{form.date.id}}_help" class="help-block">
{{messages['date']}}
</span>
{{ render_field_with_errors(form.odometer) }}
<span id="{{form.odometer.id}}_help" class="help-block">
{{messages['odometer']}}
</span>
{{ render_field_with_errors(form.litres) }}
<span id="{{form.litres.id}}_help" class="help-block">
{{messages['litres']}}
</span>
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>New Regular Cost for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.start_at) }}
<span id="{{form.start_at.id}}_help" class="help-block">
{{messages['start_at']}}
</span>
{{ render_field_with_errors(form.ends_at) }}
<span id="{{form.ends_at.id}}_help" class="help-block">
{{messages['ends_at']}}
</span>
{{ render_field_with_errors(form.days) }}
<span>Format as 'Month-Day' (e.g. 05-25) and separate with ','.</span>
<span id="{{form.days.id}}_help" class="help-block">
{{messages['days']}}
</span>
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.description) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>New Service for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.date) }}
<span id="{{form.date.id}}_help" class="help-block">
{{messages['date']}}
</span>
{{ render_field_with_errors(form.odometer) }}
<span id="{{form.odometer.id}}_help" class="help-block">
{{messages['odometer']}}
</span>
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.description) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Create vehicle</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.consumables) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Delete account for '{{current_user.email}}'?</h3>
This cannot be undone!
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Delete consumable '{{consumable.name}}'?</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'layout.html' %}
{% block body %}
<div class='col-md-2' ></div>
<div class='col-md-8'>
<div class='panel panel-default'>
<div class='panel-body'>
<h3>Delete pitstop?</h3>
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
<tr>
<th style='text-align:right'>Date of Pitstop</th>
<td style='text-align: left'>{{ pitstop.date }}</td>
</tr>
<tr>
<th style='text-align:right'>Odometer</th>
<td style='text-align: left'>{{ pitstop.odometer }} km</td>
</tr>
<tr>
<th style='text-align:right'>Litres</th>
<td style='text-align: left'>{{ pitstop.amount }} {{ pitstop.consumable.unit }}</td>
</tr>
<tr>
<th style='text-align:right'>Costs (overall)</th>
<td style='text-align: left'>
{% if pitstop.costs %}
{{pitstop.costs}}
{% else %}
--
{% endif %}
</td>
</tr>
</table>
<form class='form-horizontal' method='POST'>
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class='col-md-2' ></div>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends 'layout.html' %}
{% block body %}
<div class='col-md-2' ></div>
<div class='col-md-8'>
<div class='panel panel-default'>
<div class='panel-body'>
<h3>Delete Regular Cost?</h3>
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
<tr>
<th style='text-align:right'>Description of regular cost</th>
<td style='text-align: left'>{{ regular_cost.description }}</td>
</tr>
<tr>
<th style='text-align:right'>Costs (per instance)</th>
<td style='text-align: left'>
{% if regular_cost.costs %}
{{ regular_cost.costs }}
{% else %}
--
{% endif %}
</td>
</tr>
<tr>
<th style='text-align:right'>Days for regular costs</th>
<td style='text-align: left'>{{ regular_cost.days }}</td>
</tr>
<tr>
<th style='text-align:right'>regular costs starting from</th>
<td style='text-align: left'>{{ regular_cost.start_at }}</td>
</tr>
<tr>
<th style='text-align:right'>regular costs ending at</th>
<td style='text-align: left'>{{ regular_cost.ends_at }}</td>
</tr>
</table>
<form class='form-horizontal' method='POST'>
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class='col-md-2' ></div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'layout.html' %}
{% block body %}
<div class='col-md-2' ></div>
<div class='col-md-8'>
<div class='panel panel-default'>
<div class='panel-body'>
<h3>Delete service?</h3>
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
<tr>
<th style='text-align:right'>Date of Pitstop</th>
<td style='text-align: left'>{{ service.date }}</td>
</tr>
<tr>
<th style='text-align:right'>Odometer</th>
<td style='text-align: left'>{{ service.odometer }} km</td>
</tr>
<tr>
<th style='text-align:right'>Description</th>
<td style='text-align: left' class="markdown">{{ service.description | markdown | safe}}</td>
</tr>
<tr>
<th style='text-align:right'>Costs (overall)</th>
<td style='text-align: left'>
{% if service.costs %}
{{service.costs}}
{% else %}
--
{% endif %}
</td>
</tr>
</table>
<form class='form-horizontal' method='POST'>
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class='col-md-2' ></div>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Delete vehicle '{{vehicle.name}}'?</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit, include_cancel=True) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit consumable</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.ext_id) }}
{{ render_field_with_errors(form.unit) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit Pitstop for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.date) }}
<span id="{{form.date.id}}_help" class="help-block">
{{messages['date']}}
</span>
{{ render_field_with_errors(form.odometer) }}
<span id="{{form.odometer.id}}_help" class="help-block">
{{messages['odometer']}}
</span>
{{ render_field_with_errors(form.litres) }}
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit Regular Cost for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.start_at) }}
<span id="{{form.start_at.id}}_help" class="help-block">
{{messages['start_at']}}
</span>
{{ render_field_with_errors(form.ends_at) }}
<span id="{{form.ends_at.id}}_help" class="help-block">
{{messages['ends_at']}}
</span>
{{ render_field_with_errors(form.days) }}
<span>Format as 'Month-Day' (e.g. 05-25) and separate with ','.</span>
<span id="{{form.days.id}}_help" class="help-block">
{{messages['days']}}
</span>
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.description) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit Pitstop for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.date) }}
<span id="{{form.date.id}}_help" class="help-block">
{{messages['date']}}
</span>
{{ render_field_with_errors(form.odometer) }}
<span id="{{form.odometer.id}}_help" class="help-block">
{{messages['odometer']}}
</span>
{{ render_field_with_errors(form.description) }}
{{ render_field_with_errors(form.costs) }}
<span id="{{form.costs.id}}_help" class="help-block">
{{messages['costs']}}
</span>
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit vehicle '{{vehicle.name}}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.consumables) }}
{{ render_field_with_errors(form.is_active) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Edit Regular Cost for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

34
app/templates/index.html Normal file
View File

@ -0,0 +1,34 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="row">
<div class="col-md-4">
{{ render_login_form() }}
</div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body" >
<h1>Join the pitstop community!</h1>
<p>There are already {{ data.users}} members with {{ data.vehicles }} vehicles who have logged {{ data.kilometers }}km in {{ data.pitstops }} pitstops. They fuelled</p>
<ul>
{% for key in data.consumables %}
{% set consumable = data.consumables[key] %}
<li>{{consumable.amount}}{{consumable.unit}} of {{consumable.name}}</li>
{% endfor %}
</ul>
<p>With pitstop community you can:</p>
<ul>
<li>manage multiple vehicles</li>
<li>track each pitstop</li>
<li>get statistics about the fuel consumption</li>
</ul>
<p><a href='{{ url_for('security.register') }}'>Register your account now</a> or <a href='{{ url_for('security.login') }}'>log into your account</a>.</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,32 +1,98 @@
{% macro alert_level(km_left) -%}
{% if km_left == None %}
alert-success
{% elif km_left < 0 %}
alert-danger
{% elif km_left < 300 %}
alert-warning
{% macro navigation() -%}
{% if current_user.email %}
<li><a id='new_pitstop_link' href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li>
<li><a id='new_service_link' href='{{ url_for('select_vehicle_for_new_service') }}'>Create Service</a></li>
<li><a id='new_service_link' href='{{ url_for('select_vehicle_for_new_regular_cost') }}'>Create Regular Cost</a></li>
<li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a id='account_link' href='{{ url_for('get_account_page') }}'>Account</a></li>
<li><a id='plan_pitstop_link' href='{{ url_for('select_vehicle_for_plan_pitstop') }}'>Plan Pitstop</a></li>
{% if current_user.has_role('admin') %}
<li><a id='admin_link' href='{{ url_for('get_admin_page') }}'>Admin</a></li>
{% endif %}
<li><a id='logout_link' href='{{ url_for('security.logout') }}'>Logout</a></li>
{% else %}
alert-success
<li><a id='login_link' href='{{ url_for('security.login') }}'>Login</a></li>
<li><a id='register_link' href='{{ url_for('security.register') }}'>Register</a></li>
{% endif %}
{%- endmacro %}
{% macro service_warning() -%}
{% if data != None and data.service_info != None and 'km_left' in data.service_info %}
<div class="alert {{ alert_level(data.service_info['km_left']) }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
Service required in {{ data.service_info['km_left'] }} km. Tasks: {{ data.service_info['tasks'] }}.
{% macro render_field_with_errors(field, include_cancel=True) %}
<div class="form-group">
{% if field.type == 'SubmitField' %}
<div class="col-md-3" ></div>
<div class="col-sm-6" style="align:center">
{% if include_cancel %}
<input id="{{ field.id }}_cancel" name="{{ field.id }}_cancel" class="btn btn-default" type="submit" value="Cancel" onclick="window.history.go(-1)">
{% endif %}
<input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}">
</div>
<div class="col-md-3" ></div>
{% else %}
<label class="col-sm-6 control-label">
{{ field.label }}
</label>
<div class="col-sm-6">
{% if field.type == 'SelectField' %}
<select id="{{ field.id }}" name="{{ field.id }}" class="form-control">
{% for choice in field.choices %}
<option value="{{ choice[0] }}" {% if choice[0] == field.default %}selected="selected"{%endif%}>{{ choice[1] }}</option>
{% endfor %}
</select>
{% elif field.type == 'SelectMultipleField' %}
<select id="{{ field.id }}" name="{{ field.id }}" class="form-control" multiple="multiple">
{% for choice in field.choices %}
<option value="{{ choice[0] }}" {% if choice[0] in field.default %}selected="selected"{%endif%}>{{ choice[1] }}</option>
{% endfor %}
</select>
{% elif field.type == 'BooleanField' %}
<input class="form-control" type="checkbox" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.id }}" aria-describedby="{{ field.id }}_help" {% if field.default %}checked{% endif %}/>
{% elif field.type == 'StringField' %}
<input class="form-control" type="text" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" aria-describedby="{{ field.id }}_help" />
{% elif field.type == 'PasswordField' %}
<input class="form-control" type="password" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" aria-describedby="{{ field.id }}_help" />
{% elif field.type == 'DateField' %}
<input class="form-control" type="date" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" aria-describedby="{{ field.id }}_help" />
{% elif field.type == 'IntegerField' %}
<input class="form-control" type="number" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" step="1" aria-describedby="{{ field.id }}_help" />
{% elif field.type == 'DecimalField' %}
<input class="form-control" type="number" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" step="{{ 1 / 10 ** field.places}}" aria-describedby="{{ field.id }}_help" />
{% elif field.type == 'TextAreaField' %}
<textarea class="form-control" id="{{ field.id }}" name="{{ field.id }}">{{ field.default|none_filter }}</textarea>
{% else %}
{{ field(**kwargs)|safe }}
{% endif %}
{% if field.errors %}
<p class='error'>
{% for error in field.errors %}
{{ error }}
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
{%- endmacro %}
</div>
{% endmacro %}
{% macro render_login_form() %}
<div class="panel panel-default">
<div class="panel-body">
<h3>Login</h3>
<form class='form-horizontal' action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }}
{{ render_field_with_errors(login_user_form.email) }}
{{ render_field_with_errors(login_user_form.password) }}
{{ render_field_with_errors(login_user_form.remember) }}
{{ render_field(login_user_form.next) }}
{{ render_field_with_errors(login_user_form.submit) }}
{% if security.recoverable %}
<a href="{{ url_for_security('forgot_password') }}">Forgot password</a>
{% endif %}
</form>
</div>
</div>
{% endmacro %}
{% macro navigation() -%}
<li><a href='{{ url_for('create_pit_stop_form') }}'>Create Pitstop</a></li>
<li><a href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a href='{{ url_for('get_services') }}'>Services</a></li>
<li><a href='{{ url_for('get_manual') }}'>Manual</a></li>
{%- endmacro %}
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang=""> <![endif]-->
@ -36,22 +102,22 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Rollerverbrauch</title>
<title>refuel journal</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-57.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-60.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-72.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-76.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-114.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-120.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-144.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-152.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-180.png') }}">
<link rel="icon" type="image/png" sizes="192x192" href="{{ url_for('static', filename='android-icon-192x192.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96x96.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-57.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-60.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-72.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-76.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-114.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-120.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-144.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-152.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-180.png') }}">
<link rel="icon" type="image/png" sizes="192x192" href="{{ url_for('static', filename='img/android-icon-192x192.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='img/favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='img/favicon-96x96.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='img/favicon-16x16.png') }}">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
@ -60,9 +126,17 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="{{ url_for('static', filename='js/jquery.tablesorter.min.js') }}"></script>
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<script src="https://www.amcharts.com/lib/3/amcharts.js"></script>
<script src="https://www.amcharts.com/lib/3/serial.js"></script>
<script src="https://www.amcharts.com/lib/3/themes/patterns.js"></script>
<script src="https://openlayers.org/api/OpenLayers.js"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/fillingstations.js') }}"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
@ -74,7 +148,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('get_pit_stops') }}">Rollerverbrauch</a>
<a class="navbar-brand" href="{{ url_for('index') }}">refuel journal</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
@ -83,14 +157,49 @@
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="row topspace">
<div class="col-md-4" ></div>
<div class="col-md-4">
<div class="alert alert-{{category}} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message }}
</div>
</div>
<div class="col-md-4" ></div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="container topspace">
<div class="starter-template">
{% block body %}
{% endblock %}
</div>
</div>
<div class="container topspace">
<div class="starter-template">
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-body">
<a href="https://www.lusiardi.de/impressum/" target="_new">Impressum</a> - <a href="https://www.lusiardi.de/datenschutzerklaerung/" target="_new">Datenschutzerklärung</a>
</div>
</div>
</div>
</div>
</div>
</div>
{#
<nav class="navbar navbar-inverse navbar-fixed-bottom">
<div class="container">
<div class="navbar-footer">
<a class="navbar-brand" href="">Imprint</a>
</div>
</div>
</nav>
#}
</body>
</html>

View File

@ -1,5 +0,0 @@
{% extends "layout.html" %}
{% block body %}
Please authorize yourself!
{% endblock %}

View File

@ -1,32 +0,0 @@
{% extends "layout.html" %}
{% macro line(header, cell) -%}
<tr>
<th>
{{ header }}
</th>
<td>
{{ cell }}
</td>
</tr>
{%- endmacro %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
{{ line('Reifenluftdruck (vorne)', '1,75 - 2,0 Bar') }}
{{ line('Reifenluftdruck (hinten)', '2,0 - 2,25 Bar') }}
{{ line('Scheinwerfer', '12V 35/35W HS1-Halogen-Glühlampe') }}
{{ line('Standlicht', '12V 5W Glassockel 9mm') }}
{{ line('Blinker vorne', '12V 10W Stecksockel 15mm') }}
{{ line('Rück-/Bremslicht', '12V/21/5W Stecksockel 15mm') }}
{{ line('Blinker hinten', '12V 10W Stecksockel 15mm') }}
{{ line('Tankinhalt', 'ca. 6,0 L') }}
{{ line('Motoröl', 'SAE 15W40') }}
{{ line('Getriebeöl', 'SAE 80/90 (0,12/0,09)') }}
</table>
</div>
{% endblock %}

View File

@ -1,44 +0,0 @@
{% extends "layout.html" %}
{% block body %}
{{ service_warning() }}
<form class='form-horizontal' id='createPitStop' action="{{ url_for('create_pit_stop') }}" method='post'>
<!-- Text input-->
<div class="form-group {% if data.error['date'] %}has-error{% endif %}">
<label class="col-sm-2 control-label" for="date">Date of Pitstop</label>
<div class="col-sm-10">
<input class="form-control" id="date" name="date" placeholder="" required="" type="date" value='{{ data.last.date }}' />
<p class='error'>{{ data.error['date'] }}</p>
</div>
</div>
<!-- Text input-->
<div class="form-group {% if data.error['odometer'] %}has-error{% endif %}">
<label class="col-sm-2 control-label" for="odometer">Odometer (km)</label>
<div class="col-sm-10">
<input class="form-control" id="odometer" name="odometer" placeholder="" type="number" value='{{ data.last.odometer }}' />
<p class='error'>{{ data.error['odometer'] }}</p>
</div>
</div>
<!-- Text input-->
<div class="form-group {% if data.error['litres'] %}has-error{% endif %}">
<label class="col-sm-2 control-label" for="litres">Litres (l)</label>
<div class="col-sm-10">
<input class="form-control" id="litres" name="litres" placeholder="" type="number" step='0.1' value='{{ data.last.litres }}' />
<p class='error'>{{ data.error['litres'] }}</p>
</div>
</div>
<!-- Button (Double) -->
<div class="form-group">
<div class="controls">
<button id="buttonLogId" name="buttonLogId" class="btn btn-success" onclick="document.getElementById('create_pit_stop').submit();">Log Pitstop</button>
<button id="buttonAbortId" name="buttonAbortId" class="btn btn-warning" onclick="window.location.href='{{ url_for('get_pit_stops') }}'" type="button" >Abort</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -1,40 +1,232 @@
{% extends "layout.html" %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
{% macro regular(field, vindex, loop) -%}
<div class="panel panel-default">
<div class="panel-body">
<div style="text-align: left; font-size: 20px;">
<span class="glyphicon glyphicon-repeat" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;" />
</div>
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>
Date<br />
Days
</th>
<th>
Odometer<br />
Distance
</th>
<th>
Litres<br />
Average
</th>
<th>Description</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.description}}</td>
</tr>
{% for pitstop in data['pitstops'] %}
<tr class='pitstop'>
<td>
{{pitstop.date}}<br />
{% if pitstop.days %}{{pitstop.days}}{% else %} --{% endif %} days
</td>
<td>
{{pitstop.odometer}} km<br />
{% if pitstop.distance %}{{pitstop.distance}}{% else %} --{% endif %} km
</td>
<td>
{{pitstop.litres}} l<br />
{% if pitstop.average %}{{pitstop.average | round(2)}}{% else %} --{% endif %} l/100km
<tr>
<th>Costs</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
{% if field.costs %}
{{field.costs}} €
{% else %}
-- €
{% endif %}
</td>
</tr>
{% endfor %}
<tr>
<th>From</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.start_at}}</td>
</tr>
<tr>
<th>To</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.ends_at}}</td>
</tr>
<tr>
<th>Days</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.days}}</td>
</tr>
</table>
{% if loop.first %}
{% endif %}
<a id="vehicle_{{vindex}}_edit_regular_{{loop.index}}" href="{{ url_for('edit_regular_form', pid=field.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
<a id="vehicle_{{vindex}}_delete_regular_{{loop.index}}" href="{{ url_for('delete_regular_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
</div>
</div>
{%- endmacro %}
{% macro regular_instance(field, vindex, loop) -%}
<div class="panel panel-default">
<div class="panel-body">
<div style="text-align: left; font-size: 20px;">
<span class="glyphicon glyphicon-repeat" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
</div>
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>Date</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
</tr>
<tr>
<th>Description</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.name}}</td>
</tr>
<tr>
<th>Costs</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
{% if field.costs %}
{{field.costs}} €
{% else %}
-- €
{% endif %}
</td>
</tr>
</table>
{% if loop.first %}
{% endif %}
<a id="vehicle_{{vindex}}_edit_regular_{{loop.index}}" href="{{ url_for('edit_regular_form', pid=field.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
<a id="vehicle_{{vindex}}_end_regular_{{loop.index}}" href="{{ url_for('end_regular_form', pid=field.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span> end series
</a>
<a id="vehicle_{{vindex}}_delete_regular_{{loop.index}}" href="{{ url_for('delete_regular_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
</div>
</div>
{%- endmacro %}
{% macro pitstop(field, vindex, loop) -%}
<div class="panel panel-default">
<div class="panel-body">
<div style="text-align: left; font-size: 20px;">
<span class="glyphicon glyphicon-filter" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
</div>
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>Date</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
</tr>
<tr>
<th>Odometer</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_odo">{{field.odometer}} km</td>
</tr>
<tr>
<th>{{ field.consumable.name }}</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_anmount">{{field.amount}} {{ field.consumable.unit }}</td>
</tr>
<tr>
<th>Costs</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
{% if field.costs %}
{{field.costs}} €
{% else %}
-- €
{% endif %}
</td>
</tr>
</table>
{% if loop.first %}
{% endif %}
<a id="vehicle_{{vindex}}_edit_pitstop_{{loop.index}}" href="{{ url_for('edit_pit_stop_form', pid=field.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
<a id="vehicle_{{vindex}}_delete_pitstop_{{loop.index}}" href="{{ url_for('delete_pit_stop_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
</div>
</div>
{%- endmacro %}
{% macro service(field, vindex, loop) -%}
<div class="panel panel-default">
<div class="panel-body">
<div style="text-align: left; font-size: 20px;">
<span class="glyphicon glyphicon-wrench" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
</div>
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>Date</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
</tr>
<tr>
<th>Odometer</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_desc">{{field.odometer}} km</td>
</tr>
<tr>
<th>Description</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_anmount" class="markdown">{{field.description | markdown | safe}}</td>
</tr>
<tr>
<th>Costs</th>
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
{% if field.costs %}
{{field.costs}} €
{% else %}
-- €
{% endif %}
</td>
</tr>
</table>
{% if loop.first %}
{% endif %}
<a id="vehicle_{{vindex}}_edit_pitstop_{{loop.index}}" href="{{ url_for('edit_service_form', sid=field.id) }}" class="btn btn-primary">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
<a id="vehicle_{{vindex}}_delete_pitstop_{{loop.index}}" href="{{ url_for('delete_service_form', sid=field.id) }}" class="btn btn-primary btn-warning ">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
</div>
</div>
{%- endmacro %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
{% for vehicle in user.vehicles %}
<li {% if loop.first %}class="active" {%endif %}>
<a href="#v{{vehicle.id}}" id="i{{vehicle.id}}" data-toggle="tab">
{{ vehicle.name }}
</a>
</li>
{% endfor %}
</ul>
<div id="my-tab-content" class="tab-content">
{% for vehicle in user.vehicles %}
{% set vehicleloop = loop %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3>
{% 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 %}
{% if 'Regular' in data.__class__.__name__ %}
{{ regular_instance(data, vehicleloop.index, loop) }}
{% endif %}
{% endfor %}
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('select_consumable_for_new_pitstop', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
{% if vehicle.regulars %}
<h4>Regular Costs</h4>
{% for data in vehicle.regulars %}
{{ regular(data, vehicleloop.index, loop) }}
{% endfor %}
{% endif %}
</div>
{% endfor %}
</div>
</div>
<div class="col-md-2" ></div>
<script type="text/javascript">
jQuery(document).ready(function ($) {
$('#tabs').tab();
if(window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click()
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Plan Pitstop for '{{ vehicle.name }}'</h3>
Price comparision for {{ consumable.name }}:
<div class="table-responsive">
<table id="compare" class="table table-striped table-bordered table-condensed tablesorter">
<thead>
<tr>
<th>Filling Station</th>
<th>Price/{{ consumable.unit }}</th>
</tr>
</thead>
<tbody>
{% for offer in offers %}
<tr>
<td>
<div class="row filling_station_info " style="border: 0px">
<div class="col-md-8">
<div>{{ offer[0].name }}</div>
<div>{{ offer[0].street }} {{ offer[0].houseNumber }}</div>
<div>{{ offer[0].postCode }} {{ offer[0].place }}</div>
</div>
<div class="col-md-4" style="height: 60px;">
<img src="/static/logos/{{ offer[0].brand|lower }}.png">
</div>
</div>
</td>
<td>
{% if offer[0].open %}
{{ offer[1] }} €/{{ consumable.unit }}
{% else %}
Closed
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$("#compare").tablesorter({sortList: [[1,0]]});
$("img").error(function(){
$(this).hide();
});
});
</script>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Change password</h3>
<form class='form-horizontal' action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ change_password_form.hidden_tag() }}
{{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field_with_errors(change_password_form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Reset password</h3>
<form class='form-horizontal' action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field_with_errors(forgot_password_form.submit) }}
</form>
</div>
</div>
<div class="col-md-2" ></div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="row">
<div class="col-md-2" ></div>
<div class="col-md-8">
{{ render_login_form() }}
</div>
<div class="col-md-2" ></div>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Register</h3>
<form class='form-horizontal' action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ register_user_form.hidden_tag() }}
{{ render_field_with_errors(register_user_form.email) }}
{{ render_field_with_errors(register_user_form.password) }}
{% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field_with_errors(register_user_form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Reset password</h1>
<form class='form-horizontal' action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form">
{{ reset_password_form.hidden_tag() }}
{{ render_field_with_errors(reset_password_form.password) }}
{{ render_field_with_errors(reset_password_form.password_confirm) }}
{{ render_field(reset_password_form.submit) }}
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Select Consumable for '{{ vehicle.name }}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.consumable) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "layout.html" %}
{% block body %}
<div class="col-md-2" ></div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body">
<h3>Select Vehicle</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.vehicle) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More