Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5305caf36 | ||
|
|
e1e5fe7725 | ||
|
|
471f1415d9 | ||
|
|
99df28a015 | ||
|
|
6b861903af | ||
|
|
706f30e2e2 | ||
|
|
cc7d2620e8 | ||
|
|
161cc16498 | ||
|
|
ef976299dc | ||
|
|
60293d6a27 | ||
|
|
1e5b63d068 | ||
|
|
13a5b07df2 | ||
|
|
662e5b5b63 | ||
|
|
fd206de263 | ||
|
|
e936f1c60f | ||
|
|
a7df0aebee | ||
|
|
3e2d62b28f | ||
|
|
ca7166f731 | ||
|
|
34905921d9 | ||
|
|
038e255b56 | ||
|
|
3709eddabb | ||
|
|
c941e1bccf | ||
|
|
7bc5422687 | ||
|
|
0d8ae0cd18 | ||
|
|
3ec2e4648d | ||
|
|
267c59cbb3 | ||
|
|
d19b0a5858 | ||
|
|
729fee51e9 | ||
|
|
4261a9bb0b | ||
|
|
0303be7945 | ||
|
|
01798e9548 | ||
|
|
9eb7ec7c8a | ||
|
|
344db8d998 | ||
|
|
1240a3c64b | ||
|
|
baca41619d | ||
|
|
2e0fc3b772 | ||
|
|
1d449cb0ab | ||
|
|
4c97bd86db | ||
|
|
8057c59329 | ||
|
|
1998fcb7d8 | ||
|
|
01019c22d9 | ||
|
|
864b7822ac | ||
|
|
6f218e591f | ||
|
|
8c8803874e | ||
|
|
6ed4110ce9 | ||
|
|
a9b2781ae3 | ||
|
|
f6f1d960b7 | ||
|
|
bbfa7b9341 | ||
|
|
808e0d80d3 | ||
|
|
fc421558b0 | ||
|
|
c1adf69ca0 | ||
|
|
059c86b449 | ||
|
|
fb521194f7 | ||
|
|
ef02cc8ab3 | ||
|
|
8f99b798bb |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
.idea/
|
||||
**.DS_Store
|
||||
**.swp
|
||||
**.pyc
|
||||
+5
-2
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
@@ -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')
|
||||
@@ -1,2 +1,5 @@
|
||||
#db-sqlite3
|
||||
Flask
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Flask-Security
|
||||
Flask-WTF
|
||||
PyMySQL
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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!')
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
|
||||
});
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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 %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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,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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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,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 %}
|
||||
@@ -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 %}
|
||||
@@ -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))
|
||||
@@ -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');
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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">×</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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
Please authorize yourself!
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
+3
-3
@@ -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
|
||||
|
||||
Reference in new issue
Block a user