55 Commits

Author SHA1 Message Date
c5305caf36 switched to configurable db 2016-05-05 15:06:29 +02:00
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
471f1415d9 Removed print 2016-05-02 08:03:24 +02:00
99df28a015 Merge branch 'add_charts' into 'master'
Add charts



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



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



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



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



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



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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea/
**.DS_Store
**.swp
**.pyc

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

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
# 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.
## 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`

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()

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')

View File

@@ -1,2 +1,5 @@
#db-sqlite3
Flask
Flask
Flask-SQLAlchemy
Flask-Security
Flask-WTF
PyMySQL

View File

@@ -0,0 +1,218 @@
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
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)
from rollerverbrauch.tools import \
VehicleStats, \
db_log_add, \
db_log_delete, \
db_log_update
from rollerverbrauch.forms import \
CreatePitstopForm, \
EditVehicleForm, \
DeleteVehicleForm, \
SelectVehicleForm
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()
db_log_add(user)
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('/')
@login_required
def index():
return redirect(url_for('get_pit_stops'))
@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():
vehicle.name = form.name.data
db.session.commit()
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()
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():
new_vehicle = Vehicle(form.name.data)
db.session.add(new_vehicle)
current_user.vehicles.append(new_vehicle)
db.session.commit()
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()
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(VehicleStats(vehicle))
return render_template('statistics.html', data=stats)

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

@@ -0,0 +1,68 @@
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), unique=True)
pitstops = db.relationship(
'Pitstop'
)
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)

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)

View File

@@ -0,0 +1,46 @@
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!')

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

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

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
});
}

View File

@@ -0,0 +1,56 @@
{% 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') }}'>
<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') }}">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a>
</div>
<table class="table table-striped table-bordered">
<tr>
<th>
Vehicle
</th>
<th>
Info
</th>
<th>
Actions
</th>
</tr>
{% for vehicle in current_user.vehicles %}
<tr>
<td style="text-align:center">
{{ vehicle.name }}
</td>
<td style="text-align:center">
{{ vehicle.pitstops | length }} pitstops
</td>
<td style="text-align:center">
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}">
<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) }}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
{% else %}
&nbsp;
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block body %}
<h3>Admin</h3>
We have {{ data.users|length }} users so far:
<ul>
{% for user in data.users %}
<li>{{user.email}}</li>
{% endfor %}
</ul>
<a href='{{ url_for('security.login', _external=True) }}'>Login</a>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block 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>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block body %}
<h3>Delete vehicle '{{vehicle.name}}'</h3>
<form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.submit) }}
</form>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "layout.html" %}
{% block 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>
{% endblock %}

View File

@@ -0,0 +1,155 @@
{% 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-sm-12" style="align:center">
<input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}">
</div>
{% else %}
<label class="col-sm-6 control-label">
{{ field.label }}
</label>
<div class="col-sm-2">
{% 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 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('get_pit_stops') }}">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>
</body>
</html>

View File

@@ -12,7 +12,6 @@
{%- endmacro %}
{% block body %}
{{ service_warning() }}
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed">

View File

@@ -0,0 +1,20 @@
{% extends "layout.html" %}
{% block 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>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% 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>
<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>
</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 %}

View File

@@ -0,0 +1,13 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Change password</h1>
<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>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Send password reset instructions</h1>
<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>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Login</h1>
<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>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<h1>Register User</h1>
<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>
{% 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,11 @@
{% extends "layout.html" %}
{% block 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>
{% endblock %}

View File

@@ -0,0 +1,113 @@
{% 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_contetn" 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}}_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>
<li>
<a href="#v{{vehicle.id}}_c3" id="i{{vehicle.id}}_c3" data-toggle="tab">
Consumption
</a>
</li>
</ul>
<div id="my-tab-content" class="tab-content">
<div class="tab-pane active" 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 %}
not enough data.
{% 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 %}
not enough data.
{% endif %}
</div>
<div class="tab-pane " 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 %}
not enough data.
{% 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 %}

View File

@@ -0,0 +1,49 @@
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))

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');

View File

@@ -1 +0,0 @@

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>

View File

@@ -1,5 +0,0 @@
{% extends "layout.html" %}
{% block body %}
Please authorize yourself!
{% 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 +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 %}

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 %}

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 %}

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