Merge branch 'add_vehicles' into 'master'
Add vehicles See merge request !5
This commit is contained in:
commit
a7df0aebee
47 files changed
+652
-352
No files matched your search
@@ -1,3 +1,4 @@
|
||||
.idea/
|
||||
**.DS_Store
|
||||
**.swp
|
||||
**.pyc
|
||||
+1
-232
@@ -1,236 +1,5 @@
|
||||
from datetime import date
|
||||
from flask import Flask
|
||||
from flask import render_template, make_response
|
||||
from flask import request, redirect, g
|
||||
from flask import url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
|
||||
UserMixin, RoleMixin, login_required, roles_required, utils
|
||||
from flask.ext.security import user_registered
|
||||
from flask.ext.mail import Mail, Message
|
||||
from flask_security.core import current_user
|
||||
from flask_wtf import Form
|
||||
from wtforms import DateField, IntegerField, DecimalField
|
||||
from wtforms.validators import DataRequired, ValidationError
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
DATABASE = '/data/rollerverbrauch.db'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+DATABASE
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
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__)
|
||||
|
||||
mail = Mail(app)
|
||||
|
||||
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())
|
||||
roles = db.relationship(
|
||||
'Role',
|
||||
secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic')
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % self.email
|
||||
|
||||
|
||||
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))
|
||||
|
||||
def __init__(self, odometer, litres, date):
|
||||
self.odometer = odometer
|
||||
self.litres = litres
|
||||
self.date = date
|
||||
|
||||
def __repr__(self):
|
||||
return '<Pitstop %r km, %r l>' % (self.odometer, self.litres)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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'))
|
||||
|
||||
|
||||
def date_check(form, field):
|
||||
if field.data < form.pitstop.date:
|
||||
raise ValidationError('The new date must after %s' % form.pitstop.date)
|
||||
|
||||
|
||||
def odometer_check(form, field):
|
||||
if field.data <= form.pitstop.odometer:
|
||||
raise ValidationError('The new odometer value must be higher than %i km' % form.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 CreatePitstopForm(Form):
|
||||
date = DateField('Date of Pitstop', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
litres = DecimalField('Litres (l)', places=1, validators=[litres_check])
|
||||
pitstop = None
|
||||
|
||||
def set_pitstop(self, pitstop):
|
||||
self.pitstop = pitstop
|
||||
|
||||
|
||||
@app.route('/pitstops/createForm', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_pit_stop_form():
|
||||
last_pitstop = Pitstop.query.order_by(Pitstop.id.desc()).first()
|
||||
if last_pitstop is None:
|
||||
last_pitstop = Pitstop(0, 0, date.today())
|
||||
|
||||
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)
|
||||
db.session.commit()
|
||||
return redirect(url_for('get_pit_stops'))
|
||||
|
||||
# dynamically set values
|
||||
form.odometer.default = last_pitstop.odometer
|
||||
form.litres.default = last_pitstop.litres
|
||||
form.date.default = date.today()
|
||||
form.process()
|
||||
return render_template('newPitStopForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops', methods=['GET'])
|
||||
@login_required
|
||||
def get_pit_stops():
|
||||
data = prepare_pit_stops(Pitstop.query.all())
|
||||
g.data['pitstops'] = data
|
||||
return render_template('pitstops.html', data=g.data)
|
||||
|
||||
|
||||
@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():
|
||||
print(current_user)
|
||||
return render_template('account.html', data=g.data)
|
||||
|
||||
|
||||
@app.route('/statistics', methods=['GET'])
|
||||
@login_required
|
||||
def get_statistics():
|
||||
pitstops = Pitstop.query.all()
|
||||
count = len(pitstops)
|
||||
distance = 0
|
||||
sum_litres = 0
|
||||
average_distance = 0
|
||||
average_litres_fuelled = 0
|
||||
average_litres_used = 0
|
||||
|
||||
if count > 0:
|
||||
sum_litres = 0
|
||||
for pitstop in pitstops:
|
||||
sum_litres += pitstop.litres
|
||||
average_litres_fuelled = sum_litres/count
|
||||
if count > 1:
|
||||
distance = pitstops[-1].odometer - pitstops[0].odometer
|
||||
average_distance = distance/(count - 1)
|
||||
average_litres_used = 100 * (sum_litres-pitstops[0].litres)/distance
|
||||
g.data['distance'] = distance
|
||||
g.data['count'] = count
|
||||
g.data['litres'] = sum_litres
|
||||
g.data['averageDistance'] = average_distance
|
||||
g.data['averageListresFuelled'] = average_litres_fuelled
|
||||
g.data['averageListresUsed'] = average_litres_used
|
||||
return render_template('statistics.html', data=g.data)
|
||||
|
||||
|
||||
def prepare_pit_stops(pss):
|
||||
pitstops = []
|
||||
for pitstop_index in range(0, len(pss)):
|
||||
p = dict()
|
||||
p['odometer'] = pss[pitstop_index].odometer
|
||||
p['litres'] = pss[pitstop_index].litres
|
||||
p['date'] = pss[pitstop_index].date
|
||||
pitstops.append(p)
|
||||
for pitstop_index in range(1, len(pitstops)):
|
||||
last = pitstops[pitstop_index - 1]
|
||||
curr = pitstops[pitstop_index]
|
||||
curr['distance'] = curr['odometer'] - last['odometer']
|
||||
curr['average'] = 100 * curr['litres']/curr['distance']
|
||||
last_date = last['date']
|
||||
curr_date = curr['date']
|
||||
curr['days'] = (curr_date - last_date).days
|
||||
pitstops.reverse()
|
||||
return pitstops
|
||||
from rollerverbrauch import app
|
||||
|
||||
if __name__ == '__main__':
|
||||
DEBUG = 'DEBUG' in os.environ and os.environ['DEBUG'] != 'False'
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
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['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////data/rollerverbrauch.db'
|
||||
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
|
||||
|
||||
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)
|
||||
v = Vehicle('default vehicle')
|
||||
db.session.add(v)
|
||||
user.vehicles.append(v)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@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()
|
||||
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()
|
||||
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()
|
||||
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.today())
|
||||
|
||||
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()
|
||||
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()
|
||||
return render_template('newPitStopForm.html', form=form, vehicle = vehicle)
|
||||
|
||||
|
||||
@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():
|
||||
print(current_user)
|
||||
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)
|
||||
|
||||
File renamed without changes.
@@ -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 %r>' % 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 %r>' % 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 %r km, %r l>' % (self.odometer, self.litres)
|
||||
@@ -0,0 +1,9 @@
|
||||
from rollerverbrauch import app
|
||||
|
||||
|
||||
@app.template_filter('none_filter')
|
||||
def none_filter(value):
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value
|
||||
@@ -0,0 +1,43 @@
|
||||
from flask_wtf import Form
|
||||
from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField
|
||||
from wtforms.validators import ValidationError, Length
|
||||
|
||||
|
||||
def date_check(form, field):
|
||||
if field.data < form.pitstop.date:
|
||||
raise ValidationError('The new date must after %s' % form.pitstop.date)
|
||||
|
||||
|
||||
def odometer_check(form, field):
|
||||
if field.data <= form.pitstop.odometer:
|
||||
raise ValidationError('The new odometer value must be higher than %i km' % form.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!')
|
||||
pitstop = None
|
||||
|
||||
def set_pitstop(self, pitstop):
|
||||
self.pitstop = 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.
File renamed without changes.
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 %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Admin</h1>
|
||||
<h3>Admin</h3>
|
||||
We have {{ data.users|length }} users so far:
|
||||
<ul>
|
||||
{% for user in data.users %}
|
||||
@@ -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 %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% macro navigation() -%}
|
||||
{% if current_user.email %}
|
||||
<li><a href='{{ url_for('create_pit_stop_form') }}'>Create Pitstop</a></li>
|
||||
<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') %}
|
||||
@@ -15,19 +15,46 @@
|
||||
|
||||
{% macro render_field_with_errors(field) %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-6 control-label">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="col-sm-2">
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<p class='error'>
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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 }}" />
|
||||
{% elif field.type == 'StringField' %}
|
||||
<input class="form-control" type="text" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" />
|
||||
{% elif field.type == 'PasswordField' %}
|
||||
<input class="form-control" type="password" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" />
|
||||
{% elif field.type == 'DateField' %}
|
||||
<input class="form-control" type="date" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" />
|
||||
{% elif field.type == 'IntegerField' %}
|
||||
<input class="form-control" type="number" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" step="1" />
|
||||
{% 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}}" />
|
||||
{% else %}
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<p class='error'>
|
||||
{% for error in field.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
File renamed without changes.
+4
-3
@@ -1,12 +1,13 @@
|
||||
{% 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) }}
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.date) }}
|
||||
{{ render_field_with_errors(form.odometer) }}
|
||||
{{ render_field_with_errors(form.litres) }}
|
||||
<input type="submit" value="Go">
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
+1
-1
@@ -8,6 +8,6 @@
|
||||
{{ 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(change_password_form.submit) }}
|
||||
{{ render_field_with_errors(change_password_form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
<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(forgot_password_form.submit) }}
|
||||
{{ render_field_with_errors(forgot_password_form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
{{ render_field_with_errors(login_user_form.password) }}
|
||||
{{ render_field_with_errors(login_user_form.remember) }}
|
||||
{{ render_field(login_user_form.next) }}
|
||||
{{ render_field(login_user_form.submit) }}
|
||||
{{ render_field_with_errors(login_user_form.submit) }}
|
||||
{% if security.recoverable %}
|
||||
<a href="{{ url_for_security('forgot_password') }}">Forgot password</a>
|
||||
{% endif %}
|
||||
+1
-1
@@ -10,6 +10,6 @@
|
||||
{% if register_user_form.password_confirm %}
|
||||
{{ render_field_with_errors(register_user_form.password_confirm) }}
|
||||
{% endif %}
|
||||
{{ render_field(register_user_form.submit) }}
|
||||
{{ render_field_with_errors(register_user_form.submit) }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
File renamed without changes.
@@ -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,62 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div id="content">
|
||||
<ul id="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="my-tab-content" class="tab-content">
|
||||
{% for vehicle in data %}
|
||||
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
|
||||
<h3>{{vehicle.name}}</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Number of Pitstops:</th>
|
||||
<td>{{ vehicle.pitstop_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Logged Distance:</th>
|
||||
<td>{{ vehicle.overall_distance | round(2) }} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Average Distance:</th>
|
||||
<td>{{ vehicle.average_distance | round(2) }} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Litres fuelled:</th>
|
||||
<td>{{ vehicle.overall_litres | round(2) }} l</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Average Litres fuelled:</th>
|
||||
<td>{{ vehicle.average_litres_fuelled | round(2) }} l</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Average Litres used:</th>
|
||||
<td>{{ vehicle.average_litres_used | round(2) }} l/100km</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</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,18 @@
|
||||
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
|
||||
if self.pitstop_count > 0:
|
||||
for pitstop in vehicle.pitstops:
|
||||
self.overall_litres += pitstop.litres
|
||||
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
|
||||
@@ -1,16 +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 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,7 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Account management for {{current_user.email}}</h1>
|
||||
|
||||
<a href='{{ url_for('security.change_password') }}'>Change password</a>
|
||||
{% endblock %}
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<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,33 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<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