Compare commits

..
70 Commits
Author SHA1 Message Date
jlusiardi f82cf425fd Merge branch 'development' into 'master'
Merge preparation for Cathrine



See merge request !13
2016-05-28 20:47:26 +02:00
jlusiardi 7696fb870c Merge branch 'docker_compose' into 'development'
Docker compose



See merge request !12
2016-05-28 17:13:22 +02:00
jlusiardi 63ff5845e2 added demo config 2016-05-26 13:54:51 +02:00
jlusiardi 10494a03a9 first version for docker-compose 2016-05-26 12:08:24 +02:00
jlusiardi 2176579baa Merge branch 'implement_delete_account' into 'development'
Implement delete account



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



See merge request !10
2016-05-24 07:02:33 +02:00
jlusiardi 293ff50809 Some more UI changes 2016-05-24 06:49:39 +02:00
jlusiardi 873a28aa28 ui redesign 2016-05-22 11:47:46 +02:00
jlusiardi 1c4d73da43 Adds start page for unauthed visitors 2016-05-18 08:19:11 +02:00
jlusiardi 7eef2b6cee Adds start page for unauthed visitors 2016-05-18 08:17:53 +02:00
jlusiardi 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
jlusiardi 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
jlusiardi c5305caf36 switched to configurable db 2016-05-05 15:06:29 +02:00
jlusiardi 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
jlusiardi 471f1415d9 Removed print 2016-05-02 08:03:24 +02:00
jlusiardi 99df28a015 Merge branch 'add_charts' into 'master'
Add charts



See merge request !8
2016-05-02 08:02:33 +02:00
jlusiardi 6b861903af add consumption chart 2016-05-02 07:59:31 +02:00
jlusiardi 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
jlusiardi 161cc16498 continued work ok charts 2016-04-30 06:41:59 +02:00
jlusiardi ef976299dc Fix PEP8 error 2016-04-28 22:33:14 +02:00
jlusiardi 60293d6a27 Demo Chart 2016-04-28 07:53:53 +02:00
jlusiardi 1e5b63d068 Merge branch 'add_app_step_log' into 'master'
fixed variable name



See merge request !7
2016-04-28 07:11:10 +02:00
jlusiardi 13a5b07df2 fixed variable name 2016-04-28 07:10:44 +02:00
jlusiardi 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
jlusiardi fd206de263 add logging for performed db operations 2016-04-28 06:32:51 +02:00
jlusiardi e936f1c60f fixed typos in readme 2016-04-27 21:41:47 +02:00
jlusiardi a7df0aebee Merge branch 'add_vehicles' into 'master'
Add vehicles



See merge request !5
2016-04-27 07:24:59 +02:00
jlusiardi 3e2d62b28f improved design of the form elements 2016-04-27 07:23:28 +02:00
jlusiardi ca7166f731 fixed stats 2016-04-25 22:28:35 +02:00
jlusiardi 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
jlusiardi 038e255b56 can add pitstops to vehicles nows 2016-04-24 14:09:41 +02:00
jlusiardi 3709eddabb prevents the deletion of vehicles of other users 2016-04-24 12:42:07 +02:00
jlusiardi c941e1bccf finished modules merge 2016-04-23 23:24:25 +02:00
jlusiardi 7bc5422687 Merge branch 'create_modules' into add_vehicles
Conflicts:
	app/main.py
2016-04-23 23:22:44 +02:00
jlusiardi 0d8ae0cd18 introduces modules 2016-04-23 23:08:39 +02:00
jlusiardi 3ec2e4648d Merge branch 'user_handling' into 'master'
User handling



See merge request !4
2016-04-23 17:35:10 +02:00
jlusiardi 267c59cbb3 adds create and delete vehicle 2016-04-23 17:11:43 +02:00
jlusiardi d19b0a5858 adds edit vehicle name 2016-04-23 11:20:39 +02:00
jlusiardi 729fee51e9 Merge branch 'user_handling' into add_vehicles
Conflicts:
	app/main.py
2016-04-21 21:52:58 +02:00
jlusiardi 4261a9bb0b reworked git ignore file 2016-04-21 08:12:12 +02:00
jlusiardi 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
jlusiardi 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
jlusiardi 9eb7ec7c8a switched to environment variable for config file lookup 2016-04-19 21:09:15 +02:00
jlusiardi 344db8d998 add example config file 2016-04-19 08:19:31 +02:00
jlusiardi 1240a3c64b Users are allowed to register now
This includes sending a welcome email.
2016-04-19 07:39:17 +02:00
jlusiardi baca41619d first iteration of vehicles 2016-04-19 06:47:10 +02:00
jlusiardi 2e0fc3b772 Merge branch 'master' into user_handling
Conflicts:
	app/main.py
	app/requirements.txt
2016-04-18 21:43:01 +02:00
jlusiardi 1d449cb0ab Merge branch 'master' of code.nerd2nerd.org:n0ob/rollerverbrauch 2016-04-18 21:01:31 +02:00
jlusiardi 4c97bd86db Fixed date vs datetime 2016-04-18 20:58:20 +02:00
jlusiardi 8057c59329 Merge branch 'wtforms' into 'master'
Wtforms



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



See merge request !1
2016-04-14 19:54:40 +02:00
jlusiardi 059c86b449 add .gitignore file 2016-04-14 19:53:49 +02:00
jlusiardi fb521194f7 Reduce services feature and reformatting 2016-04-14 07:32:06 +02:00
jlusiardi ef02cc8ab3 Add basic build and run information 2016-04-14 07:02:28 +02:00
jlusiardi 8f99b798bb reworked docker file for faster build times 2016-04-13 08:30:13 +02:00
61 changed files with 1429 additions and 648 deletions
+4
View File
@@ -0,0 +1,4 @@
.idea/
**.DS_Store
**.swp
**.pyc
+5 -2
View File
@@ -1,9 +1,12 @@
FROM debian8_python3
ADD app /app
RUN pip3 install -r /app/requirements.txt; \
COPY app/requirements.txt /requirements.txt
RUN pip3 install -r /requirements.txt; \
mkdir /data
ADD app /app
VOLUME ["/data"]
VOLUME ["/app/config]
EXPOSE 5000
ENTRYPOINT python3 /app/main.py
+28
View File
@@ -0,0 +1,28 @@
# 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`
## 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/../rollerverbrauch_config:/app/config -v /tmp/pitstops/:/data -e DEBUG=True -p 5000:5000 rollerverbrauch`
## run in production
`docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch`
-92
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()
Binary file not shown.
+8 -182
View File
@@ -1,186 +1,12 @@
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
import os
from rollerverbrauch import app
import logging
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
def setup_logging():
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
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')
DEBUG = 'DEBUG' in os.environ and os.environ['DEBUG'] != 'False'
setup_logging()
app.run(debug=DEBUG, host='0.0.0.0')
+5 -2
View File
@@ -1,2 +1,5 @@
#db-sqlite3
Flask
Flask
Flask-SQLAlchemy
Flask-Security
Flask-WTF
PyMySQL
+253
View File
@@ -0,0 +1,253 @@
from datetime import date
from flask import Flask
from flask import redirect, g
from flask import render_template
from flask import url_for
from flask.ext.mail import Mail
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
UserMixin, RoleMixin, login_required, roles_required
from flask.ext.security import user_registered
from flask_security.core import current_user
from flask_sqlalchemy import SQLAlchemy
from flask.ext.security.forms import LoginForm
app = Flask(__name__)
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
app.config['SECURITY_REGISTERABLE'] = True
app.config['SECURITY_CHANGEABLE'] = True
app.config['SECURITY_RECOVERABLE'] = True
app.config.from_envvar('config')
app.config.from_object(__name__)
db = SQLAlchemy(app)
mail = Mail(app)
import rollerverbrauch.tools as tools
from rollerverbrauch.forms import \
CreatePitstopForm, \
EditVehicleForm, \
DeleteVehicleForm, \
SelectVehicleForm, \
DeleteAccountForm
from rollerverbrauch.entities import \
User, \
Role, \
Pitstop, \
Vehicle
# required to activate the filters
import rollerverbrauch.filters
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
@user_registered.connect_via(app)
def user_registered_sighandler(app, user, confirm_token):
"""
Called after a user was created
"""
role = user_datastore.find_role('user')
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)
@app.before_first_request
def before_first_request():
db.create_all()
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())
vehicles = Vehicle.query.all()
litres = 0
kilometers = 0
for vehicle in vehicles:
stats = tools.VehicleStats(vehicle)
litres += stats.overall_litres
kilometers += stats.overall_distance
vehicle_count = len(vehicles)
pitstop_count = len(Pitstop.query.all())
data = {
'users':user_count,
'vehicles': vehicle_count,
'pitstops': pitstop_count,
'litres': litres,
'kilometers': kilometers
}
return render_template('index.html', login_user_form=LoginForm(), data=data)
@app.route('/account/edit_vehicle/<int:vid>', methods=['GET', 'POST'])
@login_required
def edit_vehicle(vid):
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
form = EditVehicleForm()
if form.validate_on_submit():
if not tools.check_vehicle_name_is_unique(current_user, form.name):
return render_template('editVehicleForm.html', form=form)
vehicle.name = form.name.data
db.session.commit()
tools.db_log_update(vehicle)
return redirect(url_for('get_account_page'))
form.name.default = vehicle.name
form.process()
return render_template('editVehicleForm.html', form=form)
@app.route('/account/delete_vehicle/<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'))
form = DeleteVehicleForm()
if form.validate_on_submit():
db.session.delete(vehicle)
db.session.commit()
tools.db_log_delete(vehicle)
return redirect(url_for('get_account_page'))
return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle)
@app.route('/account/create_vehicle', methods=['GET', 'POST'])
@login_required
def create_vehicle():
form = EditVehicleForm()
if form.validate_on_submit():
vehicle_name = form.name.data
if not tools.check_vehicle_name_is_unique(current_user, form.name):
return render_template('createVehicleForm.html', form=form)
new_vehicle = Vehicle(vehicle_name)
db.session.add(new_vehicle)
current_user.vehicles.append(new_vehicle)
db.session.commit()
tools.db_log_add(new_vehicle)
return redirect(url_for('get_account_page'))
return render_template('createVehicleForm.html', form=form)
@app.route('/pitstops/select_vehicle', methods=['GET', 'POST'])
@login_required
def select_vehicle_for_new_pitstop():
form = SelectVehicleForm()
form.vehicle.choices = [(g.id, g.name) for g in current_user.vehicles]
if form.validate_on_submit():
vehicle = Vehicle.query.filter(Vehicle.id == form.vehicle.data).first()
if vehicle not in current_user.vehicles:
return render_template('selectVehice.html', form=form)
return redirect(url_for('create_pit_stop_form', vid=form.vehicle.data))
return render_template('selectVehice.html', form=form)
@app.route('/pitstops/create/<int:vid>', methods=['GET', 'POST'])
@login_required
def create_pit_stop_form(vid):
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
if vehicle not in current_user.vehicles:
return redirect(url_for('select_vehicle_for_new_pitstop'))
if len(vehicle.pitstops) > 0:
last_pitstop = vehicle.pitstops[-1]
else:
last_pitstop = Pitstop(0, 0, date(1970, 1, 1))
form = CreatePitstopForm()
form.set_pitstop(last_pitstop)
if form.validate_on_submit():
new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data)
db.session.add(new_stop)
vehicle.pitstops.append(new_stop)
db.session.commit()
tools.db_log_add(new_stop)
return redirect(url_for('get_pit_stops', _anchor= 'v' + str(vehicle.id)))
form.odometer.default = last_pitstop.odometer
form.litres.default = last_pitstop.litres
form.date.default = date.today()
form.process()
messages = {
'date': 'Date must be between %s and %s (including).' % (str(last_pitstop.date), str(date.today())),
'odometer': 'Odometer must be greater than %s km.' % (str(last_pitstop.odometer))
}
return render_template('newPitStopForm.html', form=form, vehicle=vehicle, messages = messages)
@app.route('/pitstops', methods=['GET'])
@login_required
def get_pit_stops():
return render_template('pitstops.html', user=current_user)
@app.route('/manual', methods=['GET'])
@login_required
def get_manual():
return render_template('manual.html', data=g.data)
@app.route('/admin', methods=['GET'])
@roles_required('admin')
def get_admin_page():
g.data['users'] = User.query.all()
return render_template('admin.html', data=g.data)
@app.route('/account', methods=['GET'])
@login_required
def get_account_page():
return render_template('account.html', data=g.data)
@app.route('/statistics', methods=['GET'])
@login_required
def get_statistics():
stats = []
for vehicle in current_user.vehicles:
stats.append(tools.VehicleStats(vehicle))
return render_template('statistics.html', data=stats)
@app.route('/account/delete', methods=['GET', 'POST'])
@login_required
def delete_account():
form = DeleteAccountForm()
if form.validate_on_submit():
user_datastore.delete_user(current_user)
db.session.commit()
return redirect(url_for('index'))
return render_template('deleteAccountForm.html', form=form)
@@ -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'
+69
View File
@@ -0,0 +1,69 @@
from rollerverbrauch import db
from flask.ext.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')))
class Role(db.Model, RoleMixin):
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):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
vehicles = db.relationship(
'Vehicle'
)
roles = db.relationship(
'Role',
secondary=roles_users,
backref=db.backref('users', lazy='dynamic')
)
def __repr__(self):
return '<User id="%r" email="%r" ' % (self.id, self.email)
class Vehicle(db.Model):
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'
)
__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):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date)
odometer = db.Column(db.Integer)
litres = db.Column(db.Numeric(5, 2))
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'))
def __init__(self, odometer, litres, date):
self.odometer = odometer
self.litres = litres
self.date = date
def __repr__(self):
return '<Pitstop odometer="%r" litres="%r" date="%r" vehicle_id="%r">' % (self.odometer, self.litres, self.date, self.vehicle_id)
+22
View File
@@ -0,0 +1,22 @@
from rollerverbrauch import app
import hashlib
@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)
+50
View File
@@ -0,0 +1,50 @@
from flask_wtf import Form
from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField
from wtforms.validators import ValidationError, Length
from datetime import date
def date_check(form, field):
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):
if 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')
class SelectVehicleForm(Form):
vehicle = SelectField('Vehicle', coerce=int)
submit = SubmitField(label='Do it!')
class CreatePitstopForm(Form):
date = DateField('Date of Pitstop', validators=[date_check])
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
submit = SubmitField(label='Do it!')
last_pitstop = None
def set_pitstop(self, last_pitstop):
self.last_pitstop = last_pitstop
class EditVehicleForm(Form):
name = StringField('Name', validators=[Length(1, 255)])
submit = SubmitField(label='Do it!')
class DeleteVehicleForm(Form):
submit = SubmitField(label='Do it!')
class DeleteAccountForm(Form):
submit = SubmitField(label='Really delete my account!')

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 980 B

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

+78
View File
@@ -0,0 +1,78 @@
body {
padding-top: 50px;
}
.starter-template {
padding-top: 30px;
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;
}
// 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;
}
}
+80
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
});
}
@@ -0,0 +1,66 @@
{% 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') }}' 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') }}" 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 }}
</td>
<td>
{{ vehicle.pitstops | length }} pitstops
</td>
<td>
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" 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) }}" 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">Account</div>
<div class="panel-body">
<a href='{{ url_for('delete_account') }}' class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</a>
</div>
</div>
{% endblock %}
+18
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>Admin</h3>
We have {{ data.users|length }} users so far:
<ul>
{% for user in data.users %}
<li>{{user.email}}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}
@@ -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>Create vehicle</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}
@@ -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 %}
@@ -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) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %}
@@ -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>Edit vehicle</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
{% 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.pitstops }} pitstops fuelling {{ data.litres }}l for {{ data.kilometers }}km.</p>
<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 %}
+189
View File
@@ -0,0 +1,189 @@
{% macro navigation() -%}
{% if current_user.email %}
<li><a href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li>
<li><a href='{{ url_for('get_statistics') }}'>Statistics</a></li>
<li><a href='{{ url_for('get_account_page') }}'>Account</a></li>
{% if current_user.has_role('admin') %}
<li><a href='{{ url_for('get_admin_page') }}'>Admin</a></li>
{% endif %}
<li><a href='{{ url_for('security.logout') }}'>Logout</a></li>
{% else %}
<li><a href='{{ url_for('security.login') }}'>Login</a></li>
<li><a href='{{ url_for('security.register') }}'>Register</a></li>
{% endif %}
{%- endmacro %}
{% macro render_field_with_errors(field) %}
<div class="form-group">
{% if field.type == 'SubmitField' %}
<div class="col-md-4" ></div>
<div class="col-sm-4" style="align:center">
<input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}">
</div>
<!--
<div class="col-sm-3" style="align:center">
<a class="btn btn-default" href="{{ g.data['back'] }}" role="button">Cancel</a>
</div>
-->
<div class="col-md-4" ></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] }}">{{ choice[1] }}</option>
{% endfor %}
</select>
{% elif field.type == 'BooleanField' %}
<input class="form-control" type="checkbox" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" aria-describedby="{{ field.id }}_help" />
{% 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" />
{% else %}
{{ field(**kwargs)|safe }}
{% endif %}
{% if field.errors %}
<p class='error'>
{% for error in field.errors %}
{{ error }}
{% endfor %}
</p>
{% endif %}
</div>
{% endif %}
</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 chartScript(divId, data, unit)%}
{% set hash = divId | md5 %}
data_{{ hash }} = [{% for stop in data %}{
"date": "{{stop.date}}",
"value": {{stop.value}}
}{% if not loop.last %},{%endif%}
{% endfor%}
]
var chart_{{ hash }} = createChart('{{divId}}', data_{{ hash }}, '{{unit}}');
function zoom_chart_{{ hash }}() {
chart_{{ hash }}.zoomToIndexes(
chart_{{ hash }}.dataProvider.length - 40,
chart_{{ hash }}.dataProvider.length - 1
);
}
chart_{{ hash }}.addListener("rendered", zoom_chart_{{ hash }});
zoom_chart_{{ hash }}()
{% endmacro %}
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang=""> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang=""> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang=""> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang=""> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<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') }}">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- Optional theme -->
<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>
<!-- 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') }}">
<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="{{ url_for('static', filename='main.js') }}"></script>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('index') }}">refuel journal</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
{{ navigation() }}
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
{% block body %}
{% endblock %}
</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>
@@ -12,7 +12,6 @@
{%- endmacro %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
@@ -0,0 +1,25 @@
{% 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) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,92 @@
{% extends "layout.html" %}
{% block body %}
<div id="content">
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
{% for vehicle in current_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 current_user.vehicles %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3>
{% if vehicle.pitstops %}
<div class="table-responsive">
<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>
</tr>
{% for pitstop in vehicle.pitstops|reverse %}
{% if not loop.last %}
{% set days = (pitstop.date - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].date).days %}
{% set distance = pitstop.odometer - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].odometer %}
{% set average = (pitstop.litres / distance) * 100 %}
<tr class='pitstop'>
<td>
{{pitstop.date}}<br/>
{{ days }} days
</td>
<td>
{{pitstop.odometer}} km<br/>
{{distance}} km
</td>
<td>
{{pitstop.litres}} l<br/>
{{average | round(2)}} l/100km
</td>
</tr>
{% else %}
<tr class='pitstop'>
<td>
{{pitstop.date}}<br/>
-- days
</td>
<td>
{{pitstop.odometer}} km<br/>
-- km
</td>
<td>
{{pitstop.litres}} l<br/>
-- l/100km
</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function ($) {
$('#tabs').tab();
if(window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click()
}
});
</script>
{% endblock %}
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -0,0 +1,119 @@
{% extends "layout.html" %}
{% block body %}
<div id="content">
<ul id="vehicle_tabs" class="nav nav-tabs" data-tabs="tabs">
{% for vehicle in data %}
<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="vehicle_content" class="tab-content ">
{% for vehicle in data %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3>
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>Number of Pitstops:</th>
<td>{{ vehicle.pitstop_count }}</td>
</tr>
<tr>
<th>Logged Distance:</th>
<td>{{ vehicle.overall_distance | round(2) }} km</td>
</tr>
<tr>
<th>Average Distance:</th>
<td>{{ vehicle.average_distance | round(2) }} km</td>
</tr>
<tr>
<th>Litres fuelled:</th>
<td>{{ vehicle.overall_litres | round(2) }} l</td>
</tr>
<tr>
<th>Average Litres fuelled:</th>
<td>{{ vehicle.average_litres_fuelled | round(2) }} l</td>
</tr>
<tr>
<th>Average Litres used:</th>
<td>{{ vehicle.average_litres_used | round(2) }} l/100km</td>
</tr>
</table>
</div>
<ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs">
<li class="active">
<a href="#v{{vehicle.id}}_c3" id="i{{vehicle.id}}_c3" data-toggle="tab">
Consumption
</a>
</li>
<li>
<a href="#v{{vehicle.id}}_c1" id="i{{vehicle.id}}_c1" data-toggle="tab">
Fuelled litres
</a>
</li>
<li>
<a href="#v{{vehicle.id}}_c2" id="i{{vehicle.id}}_c2" data-toggle="tab">
Odometer
</a>
</li>
</ul>
<div id="charts_tabs-content" class="tab-content">
<div class="tab-pane active" id="v{{vehicle.id}}_c3">
{% if vehicle.pitstop_count > 1 %}
<div id="averageUsageDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript">
{{ chartScript('averageUsageDiv'+vehicle.id|str, vehicle.average_litres, 'l/100 km') }}
</script>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div>
<div class="tab-pane " id="v{{vehicle.id}}_c1">
{% if vehicle.pitstop_count > 0 %}
<div id="fuelledChartDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript">
{{ chartScript('fuelledChartDiv'+vehicle.id|str, vehicle.litres, 'l') }}
</script>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div>
<div class="tab-pane " id="v{{vehicle.id}}_c2">
{% if vehicle.pitstop_count > 0 %}
<div id="odometerChartDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript">
{{ chartScript('odometerChartDiv'+vehicle.id|str, vehicle.odometers, 'km') }}
</script>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function ($) {
$('#vehicle_tabs').tab();
if(window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click()
}
});
</script>
{% endblock %}
+67
View File
@@ -0,0 +1,67 @@
import logging
class VehicleStats:
def __init__(self, vehicle):
self.name = vehicle.name
self.id = vehicle.id
self.pitstop_count = len(vehicle.pitstops)
self.overall_distance = 0
self.average_distance = 0
self.overall_litres = 0
self.average_litres_fuelled = 0
self.average_litres_used = 0
self.litres = []
self.average_litres = []
self.odometers = []
if self.pitstop_count > 0:
for pitstop in vehicle.pitstops:
self.overall_litres += pitstop.litres
self.litres.append(StatsEvent(pitstop.date, pitstop.litres))
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
self.average_litres_fuelled = self.overall_litres / self.pitstop_count
if self.pitstop_count > 1:
self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
self.average_distance = self.overall_distance / (self.pitstop_count - 1)
self.average_litres_used = 100 * (self.overall_litres - vehicle.pitstops[0].litres) / self.overall_distance
for index in range(1, self.pitstop_count):
last_ps = vehicle.pitstops[index - 1]
current_ps = vehicle.pitstops[index]
self.average_litres.append(StatsEvent(current_ps.date, round(100 * current_ps.litres/(current_ps.odometer - last_ps.odometer),2)))
class StatsEvent:
def __init__(self, date, value):
self.date = date
self.value = value
def db_log_add(entity):
logging.info('db_add: %s' % str(entity))
def db_log_delete(entity):
logging.info('db_delete: %s' % str(entity))
def db_log_update(entity):
logging.info('db_update: %s' % str(entity))
def check_vehicle_name_is_unique(current_user, name_field):
"""
Checks if the vehicle name given in the name_field is unique for the vehicles of the current user. An error is added
to the field it the name is not unique.
:param current_user: the user currently logged in
:param name_field: the form field to enter the name to
:return: True if the name is unique, False otherwise.
"""
vehicle_name = name_field.data
for vehicle in current_user.vehicles:
if vehicle.name == vehicle_name:
name_field.default = vehicle_name
name_field.errors.append('Vehicle "%s" already exists.' % vehicle_name)
return False
return True
-25
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');
-30
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;
}
-1
View File
@@ -1 +0,0 @@
-96
View File
@@ -1,96 +0,0 @@
{% macro alert_level(km_left) -%}
{% if km_left == None %}
alert-success
{% elif km_left < 0 %}
alert-danger
{% elif km_left < 300 %}
alert-warning
{% else %}
alert-success
{% 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'] }}.
</div>
{% endif %}
{%- 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]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang=""> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang=""> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang=""> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Rollerverbrauch</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') }}">
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- Optional theme -->
<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>
<!-- 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') }}">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('get_pit_stops') }}">Rollerverbrauch</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
{{ navigation() }}
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
{% block body %}
{% endblock %}
</div>
</div>
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
{% extends "layout.html" %}
{% block body %}
Please authorize yourself!
{% endblock %}
-44
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 %}
-40
View File
@@ -1,40 +0,0 @@
{% extends "layout.html" %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
<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>
</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
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
-91
View File
@@ -1,91 +0,0 @@
{% extends "layout.html" %}
{% macro print_service_header() -%}
<tr>
<th>
Odometer<br />
(planned<br />
done)
</th>
<th>
Date<br />
(done)
</th>
<th>
Tasks
</th>
</tr>
{%- endmacro %}
{% macro print_service(service) -%}
<tr class='pitstop'>
<td>
{{service.odometer_planned}} km<br />
{% if service.odometer_done %}
{{service.odometer_done}} km
{% else %}
-- km
{% endif %}
</td>
<td>
{% if service.date %}
{{service.date}}
{% else %}
--
{% endif %}
</td>
<td>
{% for task in service.tasks.split(',') %}
{{task}} <br />
{% endfor %}
</td>
</tr>
{%- endmacro %}
{% block body %}
{{ service_warning() }}
<div class="tabbable tabs-left">
<ul class="nav nav-pills" id="tabs">
<li class="active">
<a href="#tab1" data-toggle="tab">Inspektionen</a>
</li>
<li>
<a href="#tab2" data-toggle="tab">Inspektionen (erledigt)</a>
</li>
</ul>
<div class="tab-content">
<div id="tab1" class="tab-pane active">
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
{{ print_service_header() }}
{% for service in data['services'] | reverse() %}
{% if not service.odometer_done %}
{{ print_service(service) }}
{% endif %}
{% endfor %}
</table>
</div>
</div>
<div id="tab2" class="tab-pane">
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
{{ print_service_header() }}
{% for service in data['services'] %}
{% if service.odometer_done %}
{{ print_service(service) }}
{% endif %}
{% endfor %}
</table>
</div>
</div>
</div>
</div>
<script type="text/javascript">
jQuery(document).ready(function ($) {
console.log('ready')
$('.nav-tabs').tab();
});
</script>
{% endblock %}
-34
View File
@@ -1,34 +0,0 @@
{% extends "layout.html" %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">
<tr>
<th>Number of Pitstops:</th>
<td>{{ data.count }}</td>
</tr>
<tr>
<th>Logged Distance:</th>
<td>{{ data.distance | round(2) }} km</td>
</tr>
<tr>
<th>Average Distance:</th>
<td>{{ data.averageDistance | round(2) }} km</td>
</tr>
<tr>
<th>Litres fuelled:</th>
<td>{{ data.litres | round(2) }} l</td>
</tr>
<tr>
<th>Average Litres fuelled:</th>
<td>{{ data.averageListresFuelled | round(2) }} l</td>
</tr>
<tr>
<th>Average Litres used:</th>
<td>{{ data.averageListresUsed | round(2) }} l/100km</td>
</tr>
</table>
</div>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
import os
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD'])
#SQLALCHEMY_DATABASE_URI = 'sqlite:////data/rollerverbrauch.db'
MAIL_SERVER = ''
MAIL_PORT= 25
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_USERNAME = ''
MAIL_PASSWORD = ''
SECURITY_EMAIL_SENDER = ''
SECURITY_PASSWORD_SALT = 'SecretSalt'
SECRET_KEY = 'SecretKey'
SECURITY_SEND_REGISTER_EMAIL = False
+3 -3
View File
@@ -4,14 +4,14 @@ SIZES="57 60 72 76 114 120 144 152 180"
for SIZE in $SIZES
do
convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/static/apple-touch-icon-${SIZE}.png
convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/rollerverbrauch/static/apple-touch-icon-${SIZE}.png
done
convert ./icon_orig.png -resize 192x192 app/static/android-icon-192x192.png
convert ./icon_orig.png -resize 192x192 app/rollerverbrauch/static/android-icon-192x192.png
SIZES="16 32 96"
for SIZE in $SIZES
do
convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/static/favicon-${SIZE}.png
convert ./icon_orig.png -resize ${SIZE}x${SIZE} app/rollerverbrauch/static/favicon-${SIZE}.png
done
+20
View File
@@ -0,0 +1,20 @@
version: '2'
services:
rollerverbrauch:
build: .
depends_on:
- database
volumes:
- ./compose_config/:/config
environment:
- config=/config/config.py
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
ports:
- 5000
database:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=foobar123
- MYSQL_DATABASE=pitstops
ports:
- 3306