Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f4a9f49d | ||
|
|
0791a23d8c | ||
|
|
80264f1d02 | ||
|
|
a6aab66f55 | ||
|
|
7443f69fea | ||
|
|
2ce0b3a34a | ||
|
|
fc1a64ac0c | ||
|
|
06653d53b2 | ||
|
|
b539361ca7 | ||
|
|
091bfcd4ca | ||
|
|
0f7c36a804 | ||
|
|
3bfc2fc182 | ||
|
|
802af2418a | ||
|
|
cd9fc4755e | ||
|
|
158c419747 | ||
|
|
79e5fdf56b | ||
|
|
a9338805e2 | ||
|
|
7f82a288da | ||
|
|
8728d3028b | ||
|
|
bc7e1591bd | ||
|
|
673b671ab8 | ||
|
|
578b2c15d8 | ||
|
|
5cd06db1b6 | ||
|
|
3035006225 | ||
|
|
3d0bb8c83f | ||
|
|
42356137c4 | ||
|
|
4196111529 | ||
|
|
7797d6c91e | ||
|
|
cbc34a02e7 | ||
|
|
6497d4677f | ||
|
|
1d767d65a6 | ||
|
|
ae6dfa8ae6 | ||
|
|
346723ae2d | ||
|
|
6397d050ae | ||
|
|
bf4bb607d5 | ||
|
|
b3cb88e870 | ||
|
|
889a3e6476 | ||
|
|
3a2b25d3bd | ||
|
|
13090d2735 | ||
|
|
25705562ad | ||
|
|
1f2e33bec4 | ||
|
|
c42a667ca4 | ||
|
|
879669b4f7 | ||
|
|
5a961d096b | ||
|
|
d9812bcb06 | ||
|
|
8b78dbe5b7 | ||
|
|
95a7e56a32 | ||
|
|
9eb4d5ada0 | ||
|
|
f7cb273254 | ||
|
|
543d3e1658 | ||
|
|
9427ed50ad | ||
|
|
28990f27fa | ||
|
|
d55fd3d9f6 | ||
|
|
e9bb7986f4 | ||
|
|
eaacd3f42e | ||
|
|
8a99e4a616 | ||
|
|
028b52d12f | ||
|
|
958a9bdd9f | ||
|
|
b02938cafe | ||
|
|
47e81c7517 | ||
|
|
af6f271b29 | ||
|
|
c9b1a1b095 | ||
|
|
2e4c815a10 | ||
|
|
5859763a14 | ||
|
|
6e665b9f2f | ||
|
|
60a1b307e4 | ||
|
|
3f458937d9 | ||
|
|
bceb9181ee | ||
|
|
086c643365 | ||
|
|
d8c6a0a59b | ||
|
|
43d9fc412e | ||
|
|
71412bf487 | ||
|
|
dd11419305 | ||
|
|
d132dc72de | ||
|
|
43fa41aace | ||
|
|
b8a6f69090 | ||
|
|
bb7c2780e8 | ||
|
|
088d1f24a0 | ||
|
|
87b3b02f3e | ||
|
|
c8a9404bf6 | ||
|
|
4acf36112c | ||
|
|
4b73699750 | ||
|
|
d393f19b47 | ||
|
|
44b92e58b5 | ||
|
|
1e7db29de9 | ||
|
|
938a7fc140 | ||
|
|
49a45ba8a4 | ||
|
|
bcc0541be3 | ||
|
|
936fbf30f6 | ||
|
|
3f8f30f5d1 | ||
|
|
cc414092a3 |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -1,4 +1,6 @@
|
||||
.idea/
|
||||
tests/results/
|
||||
**.DS_Store
|
||||
**.swp
|
||||
**.pyc
|
||||
*.sqlite
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
FROM debian8_python3
|
||||
FROM python:3
|
||||
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN pip3 install -r /requirements.txt; \
|
||||
mkdir /data
|
||||
RUN mkdir /data; \
|
||||
pip3 install -r /requirements.txt;
|
||||
|
||||
ADD app /app
|
||||
ADD main.py /main.py
|
||||
ADD config.py /config.py
|
||||
|
||||
VOLUME ["/data"]
|
||||
VOLUME ["/app/config]
|
||||
VOLUME ["/app/config"]
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT python3 /main.py
|
||||
@@ -5,15 +5,20 @@
|
||||
|
||||
## general configuration
|
||||
|
||||
Look at *app/config/email.py.example** for the configuration of the
|
||||
Look at *app/config/email.py.example* for the configuration of the
|
||||
parameters required for sending emails. Copy the file as *email.py* to
|
||||
a folder that will serve as configuration directory and fill in the
|
||||
information. The directory will be used as volume during container
|
||||
operation.
|
||||
|
||||
## start database
|
||||
|
||||
`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mysql:latest`
|
||||
|
||||
or
|
||||
|
||||
`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mariadb:latest`
|
||||
|
||||
## Database migrations
|
||||
### From *Cathrine* to *Master*:
|
||||
|
||||
@@ -26,6 +31,6 @@ 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 --link pitstops_db:database -p 5000:5000 -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch`
|
||||
`docker run --rm --name rollerverbrauch -ti -v $PWD/app:/app -v $PWD/data:/data -p 5000:5000 -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch`
|
||||
## run in production
|
||||
`docker run --name pitstops -tid -e PROXY_DATA=server_names:ps.lusiardi.de,port:5000 --link pitstops_db:database -e SECURITY_PASSWORD_SALT=XXX -e SECRET_KEY=XXX -e MAIL_SERVER=XXX -e MAIL_USERNAME=XXX -e MAIL_PASSWORD=XXX rollerverbrauch:catherine`
|
||||
+25
-410
@@ -1,63 +1,51 @@
|
||||
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 import Flask, make_response
|
||||
from flask import g
|
||||
from flask_mail import Mail
|
||||
from flask_security import Security, SQLAlchemyUserDatastore, \
|
||||
login_required, roles_required, user_registered
|
||||
from flask_security.core import current_user
|
||||
from flask_security.forms import LoginForm
|
||||
from flask_security import Security, SQLAlchemyUserDatastore, user_registered
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
import os
|
||||
from config import config
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from flask.ext.security.forms import LoginForm
|
||||
from flask_security.forms import LoginForm
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
from .forms import \
|
||||
CreatePitstopForm, \
|
||||
EditVehicleForm, \
|
||||
DeleteVehicleForm, \
|
||||
SelectVehicleForm, \
|
||||
DeleteAccountForm, \
|
||||
DeletePitStopForm, \
|
||||
EditPitstopForm, \
|
||||
CreateConsumableForm, \
|
||||
EditConsumableForm, \
|
||||
DeletConsumableForm, \
|
||||
SelectConsumableForm
|
||||
from .forms import *
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default'])
|
||||
|
||||
|
||||
@app.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
return make_response(
|
||||
jsonify(error="ratelimit exceeded %s" % e.description)
|
||||
, 429
|
||||
)
|
||||
db = SQLAlchemy(app)
|
||||
mail = Mail(app)
|
||||
|
||||
from .entities import \
|
||||
User, \
|
||||
Role, \
|
||||
Pitstop, \
|
||||
Vehicle, \
|
||||
Consumable
|
||||
|
||||
# required to activate the filters
|
||||
from .filters import *
|
||||
from .tools import *
|
||||
|
||||
from .entities import *
|
||||
|
||||
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
||||
security = Security(app, user_datastore)
|
||||
|
||||
# required to activate the filters
|
||||
from .filters import *
|
||||
from .tools import *
|
||||
from .routes import *
|
||||
|
||||
|
||||
@user_registered.connect_via(app)
|
||||
def user_registered_sighandler(app, user, confirm_token):
|
||||
def user_registered_sighandler(application, user, confirm_token):
|
||||
"""
|
||||
Called after a user was created
|
||||
"""
|
||||
role = user_datastore.find_role('user')
|
||||
user_datastore.add_role_to_user(user, role)
|
||||
if user.email == application.config['ADMIN_MAIL']:
|
||||
# if the user selected the preconfigured email for the admin account
|
||||
role = user_datastore.find_role('admin')
|
||||
user_datastore.add_role_to_user(user, role)
|
||||
new_vehicle = Vehicle('default vehicle')
|
||||
db.session.add(new_vehicle)
|
||||
user.vehicles.append(new_vehicle)
|
||||
@@ -69,6 +57,7 @@ def user_registered_sighandler(app, user, confirm_token):
|
||||
@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()
|
||||
@@ -112,377 +101,3 @@ def index():
|
||||
|
||||
return render_template('index.html', login_user_form=LoginForm(), data=data)
|
||||
|
||||
|
||||
@app.route('/account/vehicle/edit/<int:vid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_vehicle(vid):
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
|
||||
|
||||
# prevent edit of foreign vehicles
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
form = EditVehicleForm()
|
||||
form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()]
|
||||
|
||||
if not form.consumables.data:
|
||||
form.consumables.default = [g.id for g in vehicle.consumables]
|
||||
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
vehicle.name = form.name.data
|
||||
# we cannot delete consumables where there are pitstops for => report error
|
||||
vehicle.consumables = []
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
vehicle.consumables.append(consumable)
|
||||
try:
|
||||
db.session.commit()
|
||||
tools.db_log_update(vehicle)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('editVehicleForm.html', form=form)
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
form.name.default = vehicle.name
|
||||
form.process()
|
||||
return render_template('editVehicleForm.html', form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route('/account/vehicle/delete/<int:vid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_vehicle(vid):
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
|
||||
|
||||
# prevent deletion of foreign vehicles
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
form = DeleteVehicleForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(vehicle)
|
||||
db.session.commit()
|
||||
tools.db_log_delete(vehicle)
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route('/account/vehicle/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_vehicle():
|
||||
form = EditVehicleForm()
|
||||
form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()]
|
||||
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
|
||||
if form.consumables.data:
|
||||
form.consumables.default = form.consumables.data
|
||||
else:
|
||||
form.consumables.default = []
|
||||
|
||||
if form.validate_on_submit():
|
||||
if len(form.consumables.data) == 0:
|
||||
form.consumables.errors.append('At least one consumable must be selected.')
|
||||
return render_template('createVehicleForm.html', form=form)
|
||||
|
||||
vehicle_name = form.name.data
|
||||
new_vehicle = Vehicle(vehicle_name)
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
new_vehicle.consumables.append(consumable)
|
||||
db.session.add(new_vehicle)
|
||||
current_user.vehicles.append(new_vehicle)
|
||||
try:
|
||||
db.session.commit()
|
||||
tools.db_log_add(new_vehicle)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('createVehicleForm.html', form=form)
|
||||
return redirect(url_for('get_account_page'))
|
||||
|
||||
return render_template('createVehicleForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/vehicle/select', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def select_vehicle_for_new_pitstop():
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for('select_consumable_for_new_pitstop', vid=current_user.vehicles[0].id))
|
||||
|
||||
form = SelectVehicleForm()
|
||||
form.vehicle.choices = [(g.id, g.name) for g in current_user.vehicles]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for('select_consumable_for_new_pitstop', vid=form.vehicle.data))
|
||||
|
||||
return render_template('selectVehicle.html', form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/vehicle/<int:vid>/consumable/select', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def select_consumable_for_new_pitstop(vid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('select_vehicle_for_new_pitstop'))
|
||||
|
||||
if len(vehicle.consumables) == 1:
|
||||
return redirect(url_for('create_pit_stop_form', vid=vid, cid=vehicle.consumables[0].id))
|
||||
|
||||
form = SelectConsumableForm()
|
||||
form.consumable.choices = [(g.id, g.name) for g in vehicle.consumables]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for('create_pit_stop_form', vid=vid, cid=form.consumable.data))
|
||||
|
||||
return render_template('selectConsumableForVehicle.html', vehicle=vehicle, form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/vehicle/<int:vid>/consumable/<int:cid>/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_pit_stop_form(vid, cid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('select_vehicle_for_new_pitstop'))
|
||||
|
||||
consumable = Consumable.query.get(cid)
|
||||
if consumable not in vehicle.consumables:
|
||||
return redirect(url_for('select_consumable_for_new_pitstop', vid=vid))
|
||||
|
||||
form = CreatePitstopForm()
|
||||
|
||||
# the last pitstop is required to be able to check the monotonicy of date and odometer
|
||||
last_pitstop = tools.get_latest_pitstop_for_vehicle(vid)
|
||||
last_pitstop_consumable = tools.get_latest_pitstop_for_vehicle_and_consumable(vid, cid)
|
||||
|
||||
# we can enter the same odometer if the pitstops are not equal
|
||||
form.same_odometer_allowed = (last_pitstop != last_pitstop_consumable)
|
||||
|
||||
# set the lower limits for odometer andd date and the values for amount and costs of the last stop
|
||||
form.set_pitstop(tools.compute_lower_limits_for_new_pitstop(last_pitstop, last_pitstop_consumable, cid))
|
||||
|
||||
# set the label of the litres field to make the user comfortable
|
||||
form.set_consumable(consumable)
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
form.preinit_with_data()
|
||||
|
||||
#
|
||||
# Validate should accept same odometer on different consumables
|
||||
#
|
||||
if form.validate_on_submit():
|
||||
new_stop = Pitstop(form.odometer.data, form.litres.data, form.date.data, form.costs.data, cid)
|
||||
db.session.add(new_stop)
|
||||
vehicle.pitstops.append(new_stop)
|
||||
try:
|
||||
db.session.commit()
|
||||
tools.db_log_add(new_stop)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.odometer.errors.append('Pitstop already present for %s at odometer %s km!' % (consumable.name, form.odometer.data))
|
||||
return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages())
|
||||
return redirect(url_for('get_pit_stops', _anchor= 'v' + str(vehicle.id)))
|
||||
|
||||
form.process()
|
||||
return render_template('createPitStopForm.html', form=form, vehicle=vehicle, messages=form.get_hint_messages())
|
||||
|
||||
|
||||
@app.route('/pitstops/delete/<int:pid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_pit_stop_form(pid):
|
||||
pitstop = Pitstop.query.filter(Pitstop.id == pid).first()
|
||||
if pitstop is None:
|
||||
return redirect(url_for('get_pit_stops'))
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == pitstop.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('get_pit_stops'))
|
||||
|
||||
form = DeletePitStopForm()
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(pitstop)
|
||||
db.session.commit()
|
||||
tools.db_log_delete(pitstop)
|
||||
return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id)))
|
||||
|
||||
return render_template('deletePitstopForm.html', form=form, pitstop=pitstop )
|
||||
|
||||
|
||||
@app.route('/pitstops/edit/<int:pid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_pit_stop_form(pid):
|
||||
edit_pitstop = Pitstop.query.get(pid)
|
||||
if edit_pitstop is None:
|
||||
return redirect(url_for('get_pit_stops'))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == edit_pitstop.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('get_pit_stops'))
|
||||
|
||||
last_pitstop_pos = vehicle.pitstops.index(edit_pitstop) - 1
|
||||
if last_pitstop_pos > 0:
|
||||
last_pitstop = vehicle.pitstops[last_pitstop_pos]
|
||||
else:
|
||||
last_pitstop = Pitstop(0, 0, date(1970, 1, 1), 0, 0)
|
||||
|
||||
form = EditPitstopForm()
|
||||
form.set_pitstop(last_pitstop)
|
||||
|
||||
if form.validate_on_submit():
|
||||
edit_pitstop.costs = form.costs.data
|
||||
edit_pitstop.date = form.date.data
|
||||
edit_pitstop.amount = form.litres.data
|
||||
edit_pitstop.odometer = form.odometer.data
|
||||
db.session.commit()
|
||||
tools.db_log_update(edit_pitstop)
|
||||
return redirect(url_for('get_pit_stops', _anchor='v' + str(vehicle.id)))
|
||||
|
||||
form.odometer.default = edit_pitstop.odometer
|
||||
form.litres.default = edit_pitstop.amount
|
||||
form.date.default = edit_pitstop.date
|
||||
form.costs.default = edit_pitstop.costs
|
||||
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))
|
||||
}
|
||||
if edit_pitstop.costs is not None and edit_pitstop.costs > 0:
|
||||
messages['costs'] = 'Costs must be higher than 0.01 €.'
|
||||
return render_template('editPitStopForm.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():
|
||||
users = User.query.all()
|
||||
consumables = Consumable.query.all()
|
||||
for consumable in consumables:
|
||||
consumable.in_use = len(consumable.vehicles) > 0
|
||||
return render_template('admin.html', users=users, consumables=consumables)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_consumable():
|
||||
form = CreateConsumableForm()
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
if form.unit.data is not None:
|
||||
form.unit.default = form.unit.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_consumable = Consumable(form.name.data, form.unit.data)
|
||||
db.session.add(new_consumable)
|
||||
try:
|
||||
db.session.commit()
|
||||
tools.db_log_add(new_consumable)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('createConsumableForm.html', form=form)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('createConsumableForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/delete/<int:cid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_consumable(cid):
|
||||
consumable = Consumable.query.filter(Consumable.id == cid).first()
|
||||
if consumable is None:
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
form = DeletConsumableForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(consumable)
|
||||
db.session.commit()
|
||||
tools.db_log_delete(consumable)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('deleteConsumableForm.html', form=form, consumable=consumable)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/edit/<int:cid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_consumable(cid):
|
||||
consumable = Consumable.query.filter(Consumable.id == cid).first()
|
||||
if consumable is None:
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
form = EditConsumableForm()
|
||||
|
||||
form.name.default = consumable.name
|
||||
form.unit.default = consumable.unit
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
if form.unit.data is not None:
|
||||
form.unit.default = form.unit.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
consumable.name = form.name.data
|
||||
consumable.unit = form.unit.data
|
||||
try:
|
||||
db.session.commit()
|
||||
tools.db_log_update(consumable)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('editConsumableForm.html', form=form)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('editConsumableForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/account', methods=['GET'])
|
||||
@login_required
|
||||
def get_account_page():
|
||||
return render_template('account.html', data=g.data)
|
||||
|
||||
|
||||
@app.route('/statistics', methods=['GET'])
|
||||
@login_required
|
||||
def get_statistics():
|
||||
stats = []
|
||||
for vehicle in current_user.vehicles:
|
||||
stats.append(tools.VehicleStats(vehicle))
|
||||
return render_template('statistics.html', data=stats)
|
||||
|
||||
|
||||
@app.route('/account/delete', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_account():
|
||||
form = DeleteAccountForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_datastore.delete_user(current_user)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('deleteAccountForm.html', form=form)
|
||||
+93
-39
@@ -1,19 +1,24 @@
|
||||
from app import db
|
||||
from flask_security import UserMixin, RoleMixin
|
||||
|
||||
roles_users = db.Table('roles_users',
|
||||
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
|
||||
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
|
||||
roles_users = db.Table(
|
||||
"roles_users",
|
||||
db.Column("user_id", db.Integer(), db.ForeignKey("user.id")),
|
||||
db.Column("role_id", db.Integer(), db.ForeignKey("role.id")),
|
||||
)
|
||||
|
||||
vehicles_consumables = db.Table('vehicles_consumables',
|
||||
db.Column('vehicle_id', db.Integer(), db.ForeignKey('vehicle.id')),
|
||||
db.Column('consumable_id', db.Integer(), db.ForeignKey('consumable.id')))
|
||||
vehicles_consumables = db.Table(
|
||||
"vehicles_consumables",
|
||||
db.Column("vehicle_id", db.Integer(), db.ForeignKey("vehicle.id")),
|
||||
db.Column("consumable_id", db.Integer(), db.ForeignKey("consumable.id")),
|
||||
)
|
||||
|
||||
|
||||
class Role(db.Model, RoleMixin):
|
||||
"""
|
||||
Entity to handle different roles for users: Typically user and admin exist
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer(), primary_key=True)
|
||||
name = db.Column(db.String(80), unique=True)
|
||||
description = db.Column(db.String(255))
|
||||
@@ -29,19 +34,19 @@ class User(db.Model, UserMixin):
|
||||
"""
|
||||
Entity to represent a user including login data and links to roles and vehicles.
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(255), unique=True)
|
||||
password = db.Column(db.String(255))
|
||||
active = db.Column(db.Boolean())
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
home_lat = db.Column(db.Numeric(8, 5), default=0)
|
||||
home_long = db.Column(db.Numeric(8, 5), default=0)
|
||||
home_zoom = db.Column(db.Integer(), default=0)
|
||||
|
||||
vehicles = db.relationship(
|
||||
'Vehicle'
|
||||
)
|
||||
vehicles = db.relationship("Vehicle")
|
||||
roles = db.relationship(
|
||||
'Role',
|
||||
secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic')
|
||||
"Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic")
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -57,26 +62,27 @@ class Vehicle(db.Model):
|
||||
* list of pitstops
|
||||
* list of possible consumables
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
name = db.Column(db.String(255))
|
||||
pitstops = db.relationship(
|
||||
'Pitstop'
|
||||
)
|
||||
consumables = db.relationship(
|
||||
'Consumable',
|
||||
secondary=vehicles_consumables
|
||||
)
|
||||
pitstops = db.relationship("Pitstop", order_by="asc(Pitstop.odometer)")
|
||||
services = db.relationship("Service", order_by="asc(Service.odometer)")
|
||||
regulars = db.relationship("RegularCost")
|
||||
consumables = db.relationship("Consumable", secondary=vehicles_consumables)
|
||||
is_active = db.Column(db.Boolean(), default=True)
|
||||
# allow vehicle names to be duplicated between different owners but must still be uniq for each owner
|
||||
__table_args__ = (db.UniqueConstraint('owner_id',
|
||||
'name',
|
||||
name='_owner_name_uniq'),)
|
||||
__table_args__ = (db.UniqueConstraint("owner_id", "name", name="_owner_name_uniq"),)
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return '<Vehicle id="%r" owner_id="%r" name="%r" />' % (self.id, self.owner_id, self.name)
|
||||
return '<Vehicle id="%r" owner_id="%r" name="%r" />' % (
|
||||
self.id,
|
||||
self.owner_id,
|
||||
self.name,
|
||||
)
|
||||
|
||||
|
||||
class Pitstop(db.Model):
|
||||
@@ -90,21 +96,26 @@ class Pitstop(db.Model):
|
||||
* the costs of the consumable
|
||||
* the id of the vehicle that was refuelled
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.Date)
|
||||
odometer = db.Column(db.Integer)
|
||||
consumable_id = db.Column(db.Integer, db.ForeignKey('consumable.id'))
|
||||
consumable_id = db.Column(db.Integer, db.ForeignKey("consumable.id"))
|
||||
amount = db.Column(db.Numeric(5, 2))
|
||||
costs = db.Column(db.Numeric(5, 2), default=0)
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'))
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey("vehicle.id"))
|
||||
# short cut to access the fuelled consumable of the pitstop
|
||||
consumable = db.relationship('Consumable')
|
||||
# this uniqueness constraint makes sure that for each consumable and each vehicle only one pitstop exists at the
|
||||
# same odometer
|
||||
__table_args__ = (db.UniqueConstraint('odometer',
|
||||
'consumable_id',
|
||||
'vehicle_id',
|
||||
name='_odometer_consumable_vehicle_uniq'),)
|
||||
consumable = db.relationship("Consumable")
|
||||
# this uniqueness constraint makes sure that for each consumable and each
|
||||
# vehicle only one pitstop exists at the same odometer
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
"odometer",
|
||||
"consumable_id",
|
||||
"vehicle_id",
|
||||
name="_odometer_consumable_vehicle_uniq",
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, odometer, amount, date, costs, consumable_id):
|
||||
self.odometer = odometer
|
||||
@@ -114,8 +125,31 @@ class Pitstop(db.Model):
|
||||
self.consumable_id = consumable_id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Pitstop odometer="%r" amount="%r" date="%r" vehicle_id="%r" consumable_id="%r">' % \
|
||||
(self.odometer, self.amount, self.date, self.vehicle_id, self.consumable_id)
|
||||
return '<Pitstop odometer="%r" amount="%r" date="%r" vehicle_id="%r" consumable_id="%r">' % (
|
||||
self.odometer,
|
||||
self.amount,
|
||||
self.date,
|
||||
self.vehicle_id,
|
||||
self.consumable_id,
|
||||
)
|
||||
|
||||
|
||||
class RegularCost(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey("vehicle.id"))
|
||||
description = db.Column(db.String(4096), unique=True)
|
||||
costs = db.Column(db.Numeric(10, 2), default=0)
|
||||
days = db.Column(db.String(1024))
|
||||
start_at = db.Column(db.Date)
|
||||
ends_at = db.Column(db.Date)
|
||||
|
||||
def __init__(self, vehicle_id, description, costs, days, start_at, ends_at):
|
||||
self.vehicle_id = vehicle_id
|
||||
self.description = description
|
||||
self.costs = costs
|
||||
self.days = days
|
||||
self.start_at = start_at
|
||||
self.ends_at = ends_at
|
||||
|
||||
|
||||
class Consumable(db.Model):
|
||||
@@ -125,14 +159,12 @@ class Consumable(db.Model):
|
||||
* name (must be globally unique)
|
||||
* unit
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), unique=True)
|
||||
unit = db.Column(db.String(255))
|
||||
|
||||
vehicles = db.relationship(
|
||||
'Vehicle',
|
||||
secondary=vehicles_consumables
|
||||
)
|
||||
vehicles = db.relationship("Vehicle", secondary=vehicles_consumables, viewonly=True)
|
||||
|
||||
def __init__(self, name, unit):
|
||||
self.name = name
|
||||
@@ -140,3 +172,25 @@ class Consumable(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
return '<Consumable name="%s" unit="%s" />' % (self.name, self.unit)
|
||||
|
||||
|
||||
class Service(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.Date)
|
||||
odometer = db.Column(db.Integer)
|
||||
vehicle_id = db.Column(db.Integer, db.ForeignKey("vehicle.id"))
|
||||
costs = db.Column(db.Numeric(10, 2), default=0)
|
||||
description = db.Column(db.String(4096))
|
||||
|
||||
def __init__(self, date, odometer, vehicle_id, costs, description):
|
||||
self.description = description
|
||||
self.costs = costs
|
||||
self.date = date
|
||||
self.odometer = odometer
|
||||
self.vehicle_id = vehicle_id
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
'<Service odometer="%r" date="%r" vehicle_id="%r" costs="%r" description="%r">'
|
||||
% (self.odometer, self.date, self.vehicle_id, self.costs, self.description)
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
from app import app
|
||||
import hashlib
|
||||
import markdown
|
||||
|
||||
|
||||
@app.template_filter('none_filter')
|
||||
@@ -20,3 +21,8 @@ def md5_filter(value):
|
||||
@app.template_filter('str')
|
||||
def str_filter(value):
|
||||
return str(value)
|
||||
|
||||
|
||||
@app.template_filter('markdown')
|
||||
def md(value):
|
||||
return markdown.markdown(value)
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
from flask_wtf import Form
|
||||
from wtforms import DateField, IntegerField, DecimalField, StringField, SelectField, SubmitField, SelectMultipleField, BooleanField
|
||||
from wtforms.validators import ValidationError, Length
|
||||
from datetime import date
|
||||
|
||||
|
||||
def date_check(form, field):
|
||||
"""
|
||||
Checks that the date of the pitstop is not before the date of the latest pitstop and not after today.
|
||||
|
||||
:param form: the form where the field is in
|
||||
:param field: the field to check
|
||||
:return: Nothing or a ValidationError if the limits are not kept
|
||||
"""
|
||||
if field.data < form.last_pitstop.date:
|
||||
raise ValidationError('The new date must not be before %s' % form.last_pitstop.date)
|
||||
if field.data > date.today():
|
||||
raise ValidationError('The new date must not be after %s' % date.today())
|
||||
|
||||
|
||||
def odometer_check(form, field):
|
||||
"""
|
||||
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
if not form.same_odometer_allowed and field.data <= form.last_pitstop.odometer:
|
||||
raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer)
|
||||
if form.same_odometer_allowed and field.data < form.last_pitstop.odometer:
|
||||
raise ValidationError('The new odometer value must be higher than %i km' % form.last_pitstop.odometer)
|
||||
|
||||
|
||||
def litres_check(form, field):
|
||||
if field.data is not None and field.data <= 0:
|
||||
raise ValidationError('You must fuel at least 0.1 l')
|
||||
|
||||
|
||||
def costs_check(form, field):
|
||||
if field.data is not None and field.data <= 0:
|
||||
raise ValidationError('Costs must be above 0.01 €.')
|
||||
|
||||
|
||||
def edit_costs_check(form, field):
|
||||
"""
|
||||
Costs must be given, if a default value was given to the form field.
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
costs_check_required = (form.costs.default is not None and form.costs.default > 0)
|
||||
if costs_check_required and field.data is not None and field.data <= 0:
|
||||
raise ValidationError('Costs must be above 0.01 €.')
|
||||
|
||||
|
||||
class SelectVehicleForm(Form):
|
||||
vehicle = SelectField('Vehicle', coerce=int)
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class SelectConsumableForm(Form):
|
||||
consumable = SelectField('Consumable', 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])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
submit = SubmitField(label='Do it!')
|
||||
last_pitstop = None
|
||||
same_odometer_allowed = True
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
else:
|
||||
self.date.default = date.today()
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
else:
|
||||
self.odometer.default = self.last_pitstop.odometer
|
||||
if self.litres.data:
|
||||
self.litres.default = self.litres.data
|
||||
else:
|
||||
self.litres.default = self.last_pitstop.amount
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
else:
|
||||
self.costs.default = self.last_pitstop.costs
|
||||
|
||||
def get_hint_messages(self):
|
||||
if self.same_odometer_allowed:
|
||||
or_equal = ' or equal to'
|
||||
else:
|
||||
or_equal = ''
|
||||
messages = {
|
||||
'date': 'Date must be between %s and %s (including).' % (str(self.last_pitstop.date), str(date.today())),
|
||||
'odometer': 'Odometer must be greater than%s %s km.' % (or_equal, str(self.last_pitstop.odometer)),
|
||||
'costs': 'Costs must be higher than 0.01 €.'
|
||||
}
|
||||
return messages
|
||||
|
||||
|
||||
class EditVehicleForm(Form):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
consumables = SelectMultipleField('Consumables', coerce=int,validators=[])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class DeleteVehicleForm(Form):
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class DeleteAccountForm(Form):
|
||||
submit = SubmitField(label='Really delete my account!')
|
||||
|
||||
|
||||
class DeletePitStopForm(Form):
|
||||
submit = SubmitField(label='Really delete this pitstop!')
|
||||
|
||||
|
||||
class EditPitstopForm(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])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[edit_costs_check])
|
||||
submit = SubmitField(label='Update it!')
|
||||
last_pitstop = None
|
||||
same_odometer_allowed = True
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
if self.litres.data:
|
||||
self.litres.default = self.litres.data
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
|
||||
|
||||
class CreateConsumableForm(Form):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class EditConsumableForm(Form):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class DeletConsumableForm(Form):
|
||||
submit = SubmitField(label='Do it!')
|
||||
@@ -0,0 +1,7 @@
|
||||
from .misc import *
|
||||
from .pitstop import *
|
||||
from .checks import *
|
||||
from .consumable import *
|
||||
from .service import *
|
||||
from .vehicle import *
|
||||
from .regular_cost import *
|
||||
@@ -0,0 +1,174 @@
|
||||
from wtforms.validators import ValidationError
|
||||
from datetime import date
|
||||
|
||||
|
||||
def regular_costs_days_check(form, field):
|
||||
"""
|
||||
Checks the input field to enter multiple days in the following format:
|
||||
`01-15,07-15` for Jan 15th and July 15
|
||||
"""
|
||||
days = form.days.data
|
||||
for day in days.split(","):
|
||||
day = day.strip()
|
||||
if not day:
|
||||
raise ValidationError("Missing Date after ','")
|
||||
try:
|
||||
m, d = day.split("-")
|
||||
m_i = int(m)
|
||||
d_i = int(d)
|
||||
except Exception:
|
||||
raise ValidationError("Malformed Date, must be 'Month-Day'")
|
||||
try:
|
||||
d = date(2021, m_i, d_i)
|
||||
except Exception:
|
||||
raise ValidationError("{}-{} is not a valid date".format(m, d))
|
||||
|
||||
|
||||
def odometer_date_check(form, field):
|
||||
"""
|
||||
Checks that the entered date and odometer of the pit stop is conformant to
|
||||
the existing pit stops. That means, if a pitstops date is between two
|
||||
other pit stops, the odometer should be as well.
|
||||
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
odometer = form.odometer.data
|
||||
date = form.date.data
|
||||
pitstops = form.pitstops
|
||||
|
||||
if len(pitstops) > 0:
|
||||
if date < pitstops[0].date and odometer >= pitstops[0].odometer:
|
||||
raise ValidationError(
|
||||
"The new odometer value must be less than %i km"
|
||||
% pitstops[0].odometer
|
||||
)
|
||||
|
||||
if date >= pitstops[-1].date and odometer <= pitstops[-1].odometer:
|
||||
raise ValidationError(
|
||||
"The new odometer value must be greater than %i km"
|
||||
% pitstops[-1].odometer
|
||||
)
|
||||
|
||||
if len(pitstops) > 1:
|
||||
for index in range(0, len(pitstops) - 1):
|
||||
if pitstops[index].date <= date < pitstops[index + 1].date:
|
||||
if (
|
||||
odometer <= pitstops[index].odometer
|
||||
or odometer >= pitstops[index + 1].odometer
|
||||
):
|
||||
raise ValidationError(
|
||||
"The new odometer value must be greater than %i km and less than %i km"
|
||||
% (
|
||||
pitstops[index].odometer,
|
||||
pitstops[index + 1].odometer,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def edit_odometer_date_check(form, field):
|
||||
"""
|
||||
This makes exactly the same checks as 'odometer_date_check' but the
|
||||
odometers may be the same (to change only amount and price).
|
||||
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
odometer = form.odometer.data
|
||||
date = form.date.data
|
||||
pitstops = form.pitstops
|
||||
|
||||
if len(pitstops) > 0:
|
||||
if date < pitstops[0].date and odometer > pitstops[0].odometer:
|
||||
raise ValidationError(
|
||||
"The new odometer value must be less than %i km"
|
||||
% pitstops[0].odometer
|
||||
)
|
||||
|
||||
if date >= pitstops[-1].date and odometer < pitstops[-1].odometer:
|
||||
raise ValidationError(
|
||||
"The new odometer value must be greater than %i km"
|
||||
% pitstops[-1].odometer
|
||||
)
|
||||
|
||||
if len(pitstops) > 1:
|
||||
for index in range(0, len(pitstops) - 1):
|
||||
if pitstops[index].date <= date < pitstops[index + 1].date:
|
||||
if (
|
||||
odometer < pitstops[index].odometer
|
||||
or odometer > pitstops[index + 1].odometer
|
||||
):
|
||||
raise ValidationError(
|
||||
"The new odometer value must be greater than %i km and less than %i km"
|
||||
% (
|
||||
pitstops[index].odometer,
|
||||
pitstops[index + 1].odometer,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def date_check(form, field):
|
||||
"""
|
||||
Checks that the date of the pitstop is not before the date of the latest
|
||||
pitstop and not after today.
|
||||
|
||||
:param form: the form where the field is in
|
||||
:param field: the field to check
|
||||
:return: Nothing or a ValidationError if the limits are not kept
|
||||
"""
|
||||
if field.data < form.last_pitstop.date:
|
||||
raise ValidationError(
|
||||
"The new date must not be before %s" % form.last_pitstop.date
|
||||
)
|
||||
if field.data > date.today():
|
||||
raise ValidationError(
|
||||
"The new date must not be after %s" % date.today()
|
||||
)
|
||||
|
||||
|
||||
def odometer_check(form, field):
|
||||
"""
|
||||
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
if (
|
||||
not form.same_odometer_allowed
|
||||
and field.data <= form.last_pitstop.odometer
|
||||
):
|
||||
raise ValidationError(
|
||||
"The new odometer value must be higher than %i km"
|
||||
% form.last_pitstop.odometer
|
||||
)
|
||||
if form.same_odometer_allowed and field.data < form.last_pitstop.odometer:
|
||||
raise ValidationError(
|
||||
"The new odometer value must be higher than %i km"
|
||||
% form.last_pitstop.odometer
|
||||
)
|
||||
|
||||
|
||||
def litres_check(form, field):
|
||||
if field.data is not None and field.data <= 0:
|
||||
raise ValidationError("You must fuel at least 0.1 l")
|
||||
|
||||
|
||||
def costs_check(form, field):
|
||||
if field.data is not None and field.data <= 0:
|
||||
raise ValidationError("Costs must be above 0.01 €.")
|
||||
|
||||
|
||||
def edit_costs_check(form, field):
|
||||
"""
|
||||
Costs must be given, if a default value was given to the form field.
|
||||
:param form:
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
costs_check_required = (
|
||||
form.costs.default is not None and form.costs.default > 0
|
||||
)
|
||||
if costs_check_required and field.data is not None and field.data <= 0:
|
||||
raise ValidationError("Costs must be above 0.01 €.")
|
||||
@@ -0,0 +1,24 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SelectField, StringField, SubmitField
|
||||
from wtforms.validators import Length
|
||||
|
||||
|
||||
class SelectConsumableForm(FlaskForm):
|
||||
consumable = SelectField('Consumable', coerce=int)
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class CreateConsumableForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class EditConsumableForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class DeletConsumableForm(FlaskForm):
|
||||
submit = SubmitField(label='Do it!')
|
||||
@@ -0,0 +1,6 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
|
||||
|
||||
class DeleteAccountForm(FlaskForm):
|
||||
submit = SubmitField(label='Really delete my account!')
|
||||
@@ -0,0 +1,93 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DateField, IntegerField, DecimalField, SubmitField
|
||||
|
||||
from .checks import *
|
||||
|
||||
|
||||
class DeletePitStopForm(FlaskForm):
|
||||
submit = SubmitField(label='Really delete this pitstop!')
|
||||
|
||||
|
||||
class EditPitstopForm(FlaskForm):
|
||||
date = DateField('Date of Pitstop')
|
||||
odometer = IntegerField('Odometer (km)', validators=[edit_odometer_date_check])
|
||||
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
submit = SubmitField(label='Update it!')
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
if self.litres.data:
|
||||
self.litres.default = self.litres.data
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
|
||||
def get_hint_messages(self):
|
||||
messages = {
|
||||
'litres': 'Litres must be higher than 0.01 L.',
|
||||
'costs': 'Costs must be higher than 0.01 €.'
|
||||
}
|
||||
return messages
|
||||
|
||||
|
||||
class CreatePitstopForm(FlaskForm):
|
||||
date = DateField('Date of Pitstop')
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_date_check])
|
||||
litres = DecimalField('Litres (l)', places=2, validators=[litres_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
submit = SubmitField(label='Do it!')
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
else:
|
||||
self.date.default = date.today()
|
||||
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
elif len(self.pitstops) > 0:
|
||||
self.odometer.default = self.pitstops[-1].odometer
|
||||
else:
|
||||
self.odometer.default = 0
|
||||
|
||||
if self.litres.data:
|
||||
self.litres.default = self.litres.data
|
||||
elif len(self.pitstops) > 0 and 'amount' in self.pitstops[-1].__dict__:
|
||||
self.litres.default = self.pitstops[-1].amount
|
||||
else:
|
||||
self.litres.default = 0
|
||||
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
elif len(self.pitstops) > 0:
|
||||
self.costs.default = self.pitstops[-1].costs
|
||||
else:
|
||||
self.costs.default = 0
|
||||
|
||||
def get_hint_messages(self):
|
||||
messages = {
|
||||
'litres': 'Litres must be higher than 0.01 L.',
|
||||
'costs': 'Costs must be higher than 0.01 €.'
|
||||
}
|
||||
return messages
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
DateField,
|
||||
IntegerField,
|
||||
DecimalField,
|
||||
SubmitField,
|
||||
TextAreaField,
|
||||
StringField,
|
||||
)
|
||||
from wtforms.validators import Length
|
||||
|
||||
from .checks import *
|
||||
|
||||
from wtforms.validators import Optional
|
||||
|
||||
|
||||
class DeleteRegularCostForm(FlaskForm):
|
||||
submit = SubmitField(label="Really delete this regular cost!")
|
||||
|
||||
|
||||
class EndRegularCostForm(FlaskForm):
|
||||
submit = SubmitField(label="Really end this regular cost!")
|
||||
|
||||
|
||||
class EditRegularCostForm(FlaskForm):
|
||||
start_at = DateField("Date of first instance")
|
||||
ends_at = DateField(
|
||||
"Date of last instance", validators=[Optional(strip_whitespace=True)]
|
||||
)
|
||||
|
||||
days = StringField("Days for instance", validators=[regular_costs_days_check])
|
||||
|
||||
costs = DecimalField("Costs (€, per instance)", places=2, validators=[costs_check])
|
||||
description = TextAreaField("Description", validators=[Length(1, 4096)])
|
||||
submit = SubmitField(label="Update it!")
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
if self.start_at.data:
|
||||
self.start_at.default = self.start_at.data
|
||||
if self.ends_at.data:
|
||||
self.ends_at.default = self.ends_at.data
|
||||
if self.days.data:
|
||||
self.days.default = self.days.data
|
||||
if self.description.data:
|
||||
self.description.default = self.description.data
|
||||
|
||||
def get_hint_messages(self):
|
||||
messages = {"costs": "Costs must be higher than 0.01 €."}
|
||||
return messages
|
||||
|
||||
|
||||
class CreateRegularCostForm(FlaskForm):
|
||||
start_at = DateField("Date of first instance")
|
||||
ends_at = DateField(
|
||||
"Date of last instance", validators=[Optional(strip_whitespace=True)]
|
||||
)
|
||||
|
||||
days = StringField("Days for instance", validators=[regular_costs_days_check])
|
||||
|
||||
costs = DecimalField("Costs (€, per instance)", places=2, validators=[costs_check])
|
||||
description = TextAreaField("Description", validators=[Length(1, 4096)])
|
||||
submit = SubmitField(label="Do it!")
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.start_at.data:
|
||||
self.start_at.default = self.start_at.data
|
||||
else:
|
||||
self.start_at.default = date.today()
|
||||
|
||||
if self.ends_at.data:
|
||||
self.ends_at.default = self.ends_at.data
|
||||
else:
|
||||
self.ends_at.default = None
|
||||
|
||||
if self.days.data:
|
||||
self.days.default = self.days.data
|
||||
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
else:
|
||||
self.costs.default = 0
|
||||
|
||||
if self.description.data:
|
||||
self.description.default = self.description.data
|
||||
@@ -0,0 +1,75 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DateField, IntegerField, DecimalField, SubmitField, TextAreaField
|
||||
from wtforms.validators import Length
|
||||
|
||||
from .checks import *
|
||||
|
||||
|
||||
class CreateServiceForm(FlaskForm):
|
||||
date = DateField('Date of Service')
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_date_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
description = TextAreaField('Description', validators=[Length(1, 4096)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
pitstops = []
|
||||
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
else:
|
||||
self.date.default = date.today()
|
||||
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
elif len(self.pitstops) > 0:
|
||||
self.odometer.default = self.pitstops[-1].odometer
|
||||
else:
|
||||
self.odometer.default = 0
|
||||
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
else:
|
||||
self.costs.default = 0
|
||||
|
||||
if self.description.data:
|
||||
self.description.default = self.description.data
|
||||
|
||||
|
||||
class DeleteServiceForm(FlaskForm):
|
||||
submit = SubmitField(label='Really delete this service!')
|
||||
|
||||
|
||||
class EditServiceForm(FlaskForm):
|
||||
date = DateField('Date of Service')
|
||||
odometer = IntegerField('Odometer (km)', validators=[edit_odometer_date_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
description = TextAreaField('Description', validators=[Length(1, 4096)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
self.date.default = self.date.data
|
||||
|
||||
if self.odometer.data:
|
||||
self.odometer.default = self.odometer.data
|
||||
|
||||
if self.costs.data:
|
||||
self.costs.default = self.costs.data
|
||||
|
||||
if self.description.data:
|
||||
self.description.default = self.description.data
|
||||
|
||||
def get_hint_messages(self):
|
||||
messages = {
|
||||
'litres': 'Litres must be higher than 0.01 L.',
|
||||
'costs': 'Costs must be higher than 0.01 €.'
|
||||
}
|
||||
return messages
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
SubmitField,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
BooleanField,
|
||||
)
|
||||
from wtforms.validators import Length
|
||||
|
||||
|
||||
class SelectVehicleForm(FlaskForm):
|
||||
vehicle = SelectField("Vehicle", coerce=int)
|
||||
submit = SubmitField(label="Do it!")
|
||||
|
||||
|
||||
class EditVehicleForm(FlaskForm):
|
||||
name = StringField("Name", validators=[Length(1, 255)])
|
||||
consumables = SelectMultipleField("Consumables", coerce=int, validators=[])
|
||||
is_active = BooleanField("Is active")
|
||||
submit = SubmitField(label="Do it!")
|
||||
|
||||
|
||||
class DeleteVehicleForm(FlaskForm):
|
||||
submit = SubmitField(label="Do it!")
|
||||
@@ -0,0 +1,6 @@
|
||||
from .account import *
|
||||
from .admin import *
|
||||
from .misc import *
|
||||
from .pitstop import *
|
||||
from .service import *
|
||||
from .regular_cost import *
|
||||
@@ -0,0 +1,140 @@
|
||||
from flask import url_for, redirect, render_template, request, jsonify
|
||||
from flask_security import login_required
|
||||
from flask_security.core import current_user
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
import json
|
||||
|
||||
from ..entities import Vehicle, Consumable
|
||||
from ..forms import EditVehicleForm, DeleteVehicleForm, DeleteAccountForm
|
||||
from ..tools import db_log_update, db_log_delete, db_log_add
|
||||
from .. import app, db, user_datastore
|
||||
|
||||
|
||||
@app.route("/account", methods=["GET"])
|
||||
@login_required
|
||||
def get_account_page():
|
||||
return render_template(
|
||||
"account.html",
|
||||
map_pos=(current_user.home_lat, current_user.home_long, current_user.home_zoom),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/account/vehicle/edit/<int:vid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_vehicle(vid):
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
|
||||
|
||||
# prevent edit of foreign vehicles
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = EditVehicleForm()
|
||||
form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()]
|
||||
|
||||
if not form.consumables.data:
|
||||
form.consumables.default = [g.id for g in vehicle.consumables]
|
||||
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
|
||||
if form.is_active.data is not None:
|
||||
form.is_active.default = form.is_active.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
vehicle.name = form.name.data
|
||||
vehicle.is_active = form.is_active.data
|
||||
# we cannot delete consumables where there are pitstops for => report error
|
||||
vehicle.consumables = []
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
vehicle.consumables.append(consumable)
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_update(vehicle)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template("editVehicleForm.html", form=form)
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form.name.default = vehicle.name
|
||||
form.is_active.default = vehicle.is_active
|
||||
form.process()
|
||||
return render_template("editVehicleForm.html", form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route("/account/vehicle/delete/<int:vid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_vehicle(vid):
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == vid).first()
|
||||
|
||||
# prevent deletion of foreign vehicles
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = DeleteVehicleForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(vehicle)
|
||||
db.session.commit()
|
||||
db_log_delete(vehicle)
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
return render_template("deleteVehicleForm.html", form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route("/account/vehicle/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_vehicle():
|
||||
form = EditVehicleForm()
|
||||
form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()]
|
||||
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
|
||||
if form.consumables.data:
|
||||
form.consumables.default = form.consumables.data
|
||||
else:
|
||||
form.consumables.default = []
|
||||
|
||||
if form.validate_on_submit():
|
||||
if len(form.consumables.data) == 0:
|
||||
form.consumables.errors.append("At least one consumable must be selected.")
|
||||
return render_template("createVehicleForm.html", form=form)
|
||||
|
||||
vehicle_name = form.name.data
|
||||
new_vehicle = Vehicle(vehicle_name)
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
new_vehicle.consumables.append(consumable)
|
||||
db.session.add(new_vehicle)
|
||||
current_user.vehicles.append(new_vehicle)
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_add(new_vehicle)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template("createVehicleForm.html", form=form)
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
return render_template("createVehicleForm.html", form=form)
|
||||
|
||||
|
||||
@app.route("/account/delete", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_account():
|
||||
form = DeleteAccountForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_datastore.delete_user(current_user)
|
||||
db.session.commit()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
return render_template("deleteAccountForm.html", form=form)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from flask import url_for, redirect, render_template
|
||||
from flask_security import login_required, roles_required
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from ..entities import User, Consumable
|
||||
from ..forms import CreateConsumableForm, DeletConsumableForm, EditConsumableForm
|
||||
from ..tools import db_log_update, db_log_delete, db_log_add
|
||||
from .. import app, db
|
||||
|
||||
|
||||
@app.route('/admin', methods=['GET'])
|
||||
@roles_required('admin')
|
||||
def get_admin_page():
|
||||
users = User.query.all()
|
||||
consumables = Consumable.query.all()
|
||||
for consumable in consumables:
|
||||
consumable.in_use = len(consumable.vehicles) > 0
|
||||
return render_template('admin.html', users=users, consumables=consumables)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create_consumable():
|
||||
form = CreateConsumableForm()
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
if form.unit.data is not None:
|
||||
form.unit.default = form.unit.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_consumable = Consumable(form.name.data, form.unit.data)
|
||||
db.session.add(new_consumable)
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_add(new_consumable)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('createConsumableForm.html', form=form)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('createConsumableForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/delete/<int:cid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_consumable(cid):
|
||||
consumable = Consumable.query.filter(Consumable.id == cid).first()
|
||||
if consumable is None:
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
form = DeletConsumableForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(consumable)
|
||||
db.session.commit()
|
||||
db_log_delete(consumable)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('deleteConsumableForm.html', form=form, consumable=consumable)
|
||||
|
||||
|
||||
@app.route('/admin/consumable/edit/<int:cid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_consumable(cid):
|
||||
consumable = Consumable.query.filter(Consumable.id == cid).first()
|
||||
if consumable is None:
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
form = EditConsumableForm()
|
||||
|
||||
form.name.default = consumable.name
|
||||
form.unit.default = consumable.unit
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
if form.unit.data is not None:
|
||||
form.unit.default = form.unit.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
consumable.name = form.name.data
|
||||
consumable.unit = form.unit.data
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_update(consumable)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.name.errors.append('"%s" is not unique.' % (form.name.data))
|
||||
return render_template('editConsumableForm.html', form=form)
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
return render_template('editConsumableForm.html', form=form)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from flask import render_template
|
||||
from flask_security import login_required
|
||||
from flask_security.core import current_user
|
||||
|
||||
from ..tools import VehicleStats
|
||||
from .. import app
|
||||
|
||||
|
||||
@app.route("/statistics", methods=["GET"])
|
||||
@login_required
|
||||
def get_statistics():
|
||||
stats = []
|
||||
|
||||
def key(v):
|
||||
return (not v.is_active, v.name)
|
||||
|
||||
vehicles = sorted(current_user.vehicles, key=key)
|
||||
for vehicle in vehicles:
|
||||
stats.append(VehicleStats(vehicle))
|
||||
return render_template("statistics.html", data=stats)
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
from flask import url_for, redirect, render_template, flash
|
||||
from flask_security import login_required
|
||||
from flask_security.core import current_user
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from datetime import date
|
||||
import types
|
||||
|
||||
|
||||
from ..entities import Vehicle, Consumable, Pitstop
|
||||
from ..forms import (
|
||||
SelectVehicleForm,
|
||||
SelectConsumableForm,
|
||||
CreatePitstopForm,
|
||||
EditPitstopForm,
|
||||
DeletePitStopForm,
|
||||
)
|
||||
from ..tools import (
|
||||
db_log_update,
|
||||
db_log_delete,
|
||||
db_log_add,
|
||||
pitstop_service_key,
|
||||
get_event_line_for_vehicle,
|
||||
RegularCostInstance,
|
||||
calculate_regular_cost_instances,
|
||||
get_users_active_vehicle,
|
||||
)
|
||||
from .. import app, db
|
||||
|
||||
|
||||
@app.route("/pitstops/vehicle/select", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def select_vehicle_for_new_pitstop():
|
||||
active_vehicles = get_users_active_vehicle(current_user)
|
||||
if len(active_vehicles) == 1:
|
||||
return redirect(
|
||||
url_for(
|
||||
"select_consumable_for_new_pitstop", vid=active_vehicles[0].id
|
||||
)
|
||||
)
|
||||
|
||||
form = SelectVehicleForm()
|
||||
form.vehicle.choices = [
|
||||
(g.id, g.name) for g in active_vehicles
|
||||
]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for("select_consumable_for_new_pitstop", vid=form.vehicle.data)
|
||||
)
|
||||
|
||||
return render_template("selectVehicle.html", form=form)
|
||||
|
||||
|
||||
@app.route("/pitstops/vehicle/<int:vid>/consumable/select", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def select_consumable_for_new_pitstop(vid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("select_vehicle_for_new_pitstop"))
|
||||
|
||||
if len(vehicle.consumables) == 0:
|
||||
flash("Please choose at least one consumable!", "warning")
|
||||
return redirect(url_for("edit_vehicle", vid=vid))
|
||||
|
||||
if len(vehicle.consumables) == 1:
|
||||
return redirect(
|
||||
url_for("create_pit_stop_form", vid=vid, cid=vehicle.consumables[0].id)
|
||||
)
|
||||
|
||||
form = SelectConsumableForm()
|
||||
form.consumable.choices = [(g.id, g.name) for g in vehicle.consumables]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for("create_pit_stop_form", vid=vid, cid=form.consumable.data)
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"selectConsumableForVehicle.html", vehicle=vehicle, form=form
|
||||
)
|
||||
|
||||
|
||||
@app.route(
|
||||
"/pitstops/vehicle/<int:vid>/consumable/<int:cid>/create", methods=["GET", "POST"]
|
||||
)
|
||||
@login_required
|
||||
def create_pit_stop_form(vid, cid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("select_vehicle_for_new_pitstop"))
|
||||
|
||||
consumable = Consumable.query.get(cid)
|
||||
if consumable not in vehicle.consumables:
|
||||
return redirect(url_for("select_consumable_for_new_pitstop", vid=vid))
|
||||
|
||||
form = CreatePitstopForm()
|
||||
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
if len(data) > 0:
|
||||
form.set_pitstops(data)
|
||||
form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (
|
||||
data[-1].consumable.id != cid
|
||||
)
|
||||
else:
|
||||
form.set_pitstops([])
|
||||
form.same_odometer_allowed = True
|
||||
|
||||
# set the label of the litres field to make the user comfortable
|
||||
form.set_consumable(consumable)
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
form.preinit_with_data()
|
||||
|
||||
#
|
||||
# Validate should accept same odometer on different consumables
|
||||
#
|
||||
if form.validate_on_submit():
|
||||
new_stop = Pitstop(
|
||||
form.odometer.data, form.litres.data, form.date.data, form.costs.data, cid
|
||||
)
|
||||
db.session.add(new_stop)
|
||||
vehicle.pitstops.append(new_stop)
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_add(new_stop)
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
form.odometer.errors.append(
|
||||
"Pitstop already present for %s at odometer %s km!"
|
||||
% (consumable.name, form.odometer.data)
|
||||
)
|
||||
return render_template(
|
||||
"createPitStopForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.process()
|
||||
return render_template(
|
||||
"createPitStopForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/pitstops/delete/<int:pid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_pit_stop_form(pid):
|
||||
pitstop = Pitstop.query.filter(Pitstop.id == pid).first()
|
||||
if pitstop is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == pitstop.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = DeletePitStopForm()
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(pitstop)
|
||||
db.session.commit()
|
||||
db_log_delete(pitstop)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template("deletePitstopForm.html", form=form, pitstop=pitstop)
|
||||
|
||||
|
||||
@app.route("/pitstops/edit/<int:pid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_pit_stop_form(pid):
|
||||
edit_pitstop = Pitstop.query.get(pid)
|
||||
if edit_pitstop is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == edit_pitstop.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = EditPitstopForm()
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
data = [x for x in data if x != edit_pitstop]
|
||||
form.set_pitstops(data)
|
||||
if not form.is_submitted():
|
||||
form.odometer.default = edit_pitstop.odometer
|
||||
form.litres.default = edit_pitstop.amount
|
||||
form.date.default = edit_pitstop.date
|
||||
form.costs.default = edit_pitstop.costs
|
||||
if form.validate_on_submit():
|
||||
edit_pitstop.costs = form.costs.data
|
||||
edit_pitstop.date = form.date.data
|
||||
edit_pitstop.amount = form.litres.data
|
||||
edit_pitstop.odometer = form.odometer.data
|
||||
db.session.commit()
|
||||
db_log_update(edit_pitstop)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.preinit_with_data()
|
||||
form.process()
|
||||
return render_template(
|
||||
"editPitStopForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/pitstops", methods=["GET"])
|
||||
@login_required
|
||||
def get_pit_stops():
|
||||
user = {"vehicles": []}
|
||||
for vehicle in current_user.vehicles:
|
||||
data = []
|
||||
for pitstop in vehicle.pitstops:
|
||||
data.append(pitstop)
|
||||
for service in vehicle.services:
|
||||
data.append(service)
|
||||
for regular_instance in calculate_regular_cost_instances(vehicle):
|
||||
data.append(regular_instance)
|
||||
|
||||
data.sort(key=pitstop_service_key)
|
||||
v = {
|
||||
"id": vehicle.id,
|
||||
"name": vehicle.name,
|
||||
"data": data,
|
||||
"regulars": vehicle.regulars,
|
||||
}
|
||||
user["vehicles"].append(v)
|
||||
|
||||
return render_template("pitstops.html", user=user)
|
||||
@@ -0,0 +1,161 @@
|
||||
from flask import url_for, redirect, render_template, flash
|
||||
from flask_security import login_required
|
||||
from flask_security.core import current_user
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from datetime import date
|
||||
|
||||
from ..entities import Vehicle, Consumable, Pitstop, RegularCost
|
||||
from ..forms import (
|
||||
SelectVehicleForm,
|
||||
CreateRegularCostForm,
|
||||
DeleteRegularCostForm,
|
||||
EditRegularCostForm,
|
||||
EndRegularCostForm,
|
||||
)
|
||||
from ..tools import (
|
||||
db_log_update,
|
||||
db_log_delete,
|
||||
db_log_add,
|
||||
pitstop_service_key,
|
||||
get_event_line_for_vehicle,
|
||||
get_users_active_vehicle,
|
||||
)
|
||||
from .. import app, db
|
||||
|
||||
|
||||
@app.route("/regular_costs/delete/<int:pid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_regular_form(pid):
|
||||
regular_cost = RegularCost.query.filter(RegularCost.id == pid).first()
|
||||
if regular_cost is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == regular_cost.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = DeleteRegularCostForm()
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(regular_cost)
|
||||
db.session.commit()
|
||||
db_log_delete(regular_cost)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template(
|
||||
"deleteRegularCostForm.html", form=form, regular_cost=regular_cost
|
||||
)
|
||||
|
||||
|
||||
@app.route("/regular_costs/vehicle/select", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def select_vehicle_for_new_regular_cost():
|
||||
active_vehicles = get_users_active_vehicle(current_user)
|
||||
if len(active_vehicles) == 1:
|
||||
return redirect(
|
||||
url_for("create_regular_cost_for_vehicle", vid=active_vehicles[0].id)
|
||||
)
|
||||
|
||||
form = SelectVehicleForm()
|
||||
form.vehicle.choices = [(g.id, g.name) for g in active_vehicles]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(
|
||||
url_for("create_regular_cost_for_vehicle", vid=form.vehicle.data)
|
||||
)
|
||||
|
||||
return render_template("selectVehicle.html", form=form)
|
||||
|
||||
|
||||
@app.route("/regular_costs/vehicle/<int:vid>/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_regular_cost_for_vehicle(vid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = CreateRegularCostForm()
|
||||
|
||||
form.preinit_with_data()
|
||||
|
||||
if form.validate_on_submit():
|
||||
regular_cost = RegularCost(
|
||||
vid,
|
||||
form.description.data,
|
||||
form.costs.data,
|
||||
form.days.data,
|
||||
form.start_at.data,
|
||||
form.ends_at.data,
|
||||
)
|
||||
db.session.add(regular_cost)
|
||||
vehicle.regulars.append(regular_cost)
|
||||
db.session.commit()
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.process()
|
||||
return render_template(
|
||||
"createRegularCostForm.html", form=form, vehicle=vehicle, messages=[]
|
||||
)
|
||||
|
||||
|
||||
@app.route("/regular_costs/edit/<int:pid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_regular_form(pid):
|
||||
edit_regular = RegularCost.query.get(pid)
|
||||
if edit_regular is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == edit_regular.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = EditRegularCostForm()
|
||||
form.preinit_with_data()
|
||||
|
||||
if not form.is_submitted():
|
||||
form.costs.default = edit_regular.costs
|
||||
form.start_at.default = edit_regular.start_at
|
||||
form.ends_at.default = edit_regular.ends_at
|
||||
form.days.default = edit_regular.days
|
||||
form.description.default = edit_regular.description
|
||||
|
||||
if form.validate_on_submit():
|
||||
edit_regular.start_at = form.start_at.data
|
||||
edit_regular.ends_at = form.ends_at.data
|
||||
edit_regular.costs = form.costs.data
|
||||
edit_regular.days = form.days.data
|
||||
edit_regular.description = form.description.data
|
||||
db.session.commit()
|
||||
db_log_update(edit_regular)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.preinit_with_data()
|
||||
form.process()
|
||||
return render_template(
|
||||
"editRegularCostForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/regular_costs/end/<int:pid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def end_regular_form(pid):
|
||||
edit_regular = RegularCost.query.get(pid)
|
||||
if edit_regular is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == edit_regular.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = EndRegularCostForm()
|
||||
if form.validate_on_submit():
|
||||
edit_regular.ends_at = date.today()
|
||||
db.session.commit()
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template(
|
||||
"endRegularCostForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
from flask import url_for, redirect, render_template
|
||||
from flask_security import login_required, current_user
|
||||
from datetime import date
|
||||
|
||||
from ..entities import Vehicle, Service
|
||||
from ..forms import (
|
||||
CreateServiceForm,
|
||||
DeleteServiceForm,
|
||||
EditServiceForm,
|
||||
SelectVehicleForm,
|
||||
)
|
||||
from ..tools import (
|
||||
db_log_update,
|
||||
db_log_delete,
|
||||
get_event_line_for_vehicle,
|
||||
get_latest_pitstop_for_vehicle,
|
||||
get_users_active_vehicle,
|
||||
)
|
||||
from .. import app, db
|
||||
|
||||
|
||||
@app.route("/service/vehicle/<int:vid>/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_service_for_vehicle(vid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = CreateServiceForm()
|
||||
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
if len(data) > 0:
|
||||
form.set_pitstops(data)
|
||||
form.same_odometer_allowed = type(data[-1]) != Service
|
||||
else:
|
||||
form.set_pitstops([])
|
||||
form.same_odometer_allowed = True
|
||||
|
||||
form.preinit_with_data()
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_service = Service(
|
||||
form.date.data,
|
||||
form.odometer.data,
|
||||
vid,
|
||||
form.costs.data,
|
||||
form.description.data,
|
||||
)
|
||||
db.session.add(new_service)
|
||||
vehicle.services.append(new_service)
|
||||
db.session.commit()
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.process()
|
||||
return render_template(
|
||||
"createServiceForm.html", form=form, vehicle=vehicle, messages=[]
|
||||
)
|
||||
|
||||
|
||||
@app.route("/service/delete/<int:sid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_service_form(sid):
|
||||
service = Service.query.filter(Service.id == sid).first()
|
||||
if service is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == service.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = DeleteServiceForm()
|
||||
if form.validate_on_submit():
|
||||
db.session.delete(service)
|
||||
db.session.commit()
|
||||
db_log_delete(service)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template("deleteServiceForm.html", form=form, service=service)
|
||||
|
||||
|
||||
@app.route("/service/edit/<int:sid>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_service_form(sid):
|
||||
edit_service = Service.query.get(sid)
|
||||
if edit_service is None:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
vehicle = Vehicle.query.filter(Vehicle.id == edit_service.vehicle_id).first()
|
||||
if vehicle not in current_user.vehicles:
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
data = [x for x in data if x != edit_service]
|
||||
form = EditServiceForm()
|
||||
form.same_odometer_allowed = True
|
||||
form.set_pitstops(data)
|
||||
|
||||
if not form.is_submitted():
|
||||
form.odometer.default = edit_service.odometer
|
||||
form.description.default = edit_service.description
|
||||
form.date.default = edit_service.date
|
||||
form.costs.default = edit_service.costs
|
||||
if form.validate_on_submit():
|
||||
edit_service.costs = form.costs.data
|
||||
edit_service.date = form.date.data
|
||||
edit_service.description = form.description.data
|
||||
edit_service.odometer = form.odometer.data
|
||||
db.session.commit()
|
||||
db_log_update(edit_service)
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.preinit_with_data()
|
||||
form.process()
|
||||
return render_template(
|
||||
"editServiceForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/service/vehicle/select", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def select_vehicle_for_new_service():
|
||||
active_vehicles = get_users_active_vehicle(current_user)
|
||||
if len(active_vehicles) == 1:
|
||||
return redirect(
|
||||
url_for("create_service_for_vehicle", vid=active_vehicles[0].id)
|
||||
)
|
||||
|
||||
form = SelectVehicleForm()
|
||||
form.vehicle.choices = [(g.id, g.name) for g in active_vehicles]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for("create_service_for_vehicle", vid=form.vehicle.data))
|
||||
|
||||
return render_template("selectVehicle.html", form=form)
|
||||
@@ -1,10 +1,19 @@
|
||||
.markdown > h1 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.starter-template {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 60px;
|
||||
padding-top: 0px;
|
||||
// padding-bottom: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -59,6 +68,14 @@ h3:after{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-bottom:0px;
|
||||
}
|
||||
|
||||
.topspace {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
// for small devices
|
||||
@media only screen
|
||||
and (min-device-width : 320px)
|
||||
@@ -75,4 +92,21 @@ and (max-device-width : 568px) {
|
||||
#charts_tabs-content {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* styling for sortable tables
|
||||
*/
|
||||
th.header {
|
||||
background-image: url(../img/updown.gif);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
}
|
||||
|
||||
th.headerSortUp {
|
||||
background-image: url(../img/down.gif);
|
||||
}
|
||||
|
||||
th.headerSortDown {
|
||||
background-image: url(../img/up.gif);
|
||||
}
|
||||
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.
Binary file not shown.
|
After Width: | Height: | Size: 54 B |
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
|
After Width: | Height: | Size: 54 B |
Binary file not shown.
|
After Width: | Height: | Size: 64 B |
Vendored
-4
@@ -1,4 +0,0 @@
|
||||
/*! jQuery v1.11.2 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
|
||||
!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.2",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(Line truncated
|
||||
return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var
Line truncated
|
||||
}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(aLine truncated
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
|
||||
(function($){$.extend({tablesorter:new
|
||||
function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHLine truncated
|
||||
var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$heaLine truncated
|
||||
@@ -12,7 +12,7 @@ function createChart(id, data, unit) {
|
||||
"axisAlpha": 0,
|
||||
"position": "left",
|
||||
"ignoreAxisWidth":true,
|
||||
// "title": unit
|
||||
"title": unit
|
||||
}],
|
||||
"balloon": {
|
||||
"borderThickness": 1,
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
/* Modernizr 2.8.3 (Custom Build) | MIT & BSD
|
||||
* Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load
|
||||
*/
|
||||
;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d<e;d++)u[c[d]]=c[d]in k;return u.list&&(u.list=!!b.createElement("datalist")&&!!a.HTMLDataListElement),u}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),e.inputtypes=function(a){for(var d=0,e,f,h,i=a.length;d<i;d++)k.setAttribute("type",f=a[d]),e=k.type!=="text",e&&(k.value=l,k.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(f)&&k.style.WebkitAppearance!==c?(g.appendChild(k),h=b.defaultView,e=h.getComputedStyle&&h.getComputedStyle(k,null).WebkitAppearance!=="textfield"&&k.offsetHeight!==0,g.removeChild(k)):/^(search|tel)$/.test(f)||(/^(url|email)$/.test(f)?e=k.checkValidity&&k.checkValidity()===!1:e=k.value!=l)),t[a[d]]=!!e;return t}("search tel url email datetime date month week time datetime-local number range color".split(" "))}var d="2.8.3",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k=b.createElement("input"),l=":)",m={}.toString,n=" -webkit- -moz- -o- -ms- ".split(" "),o="Webkit Moz O ms",p=o.split(" "),q=o.toLowerCase().split(" "),r={svg:"http://www.w3.org/2000/svg"},s={},t={},u={},v=[],w=v.slice,x,y=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'<style id="s',h,'">',a,"</style>"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b)&&c(b).matches||!1;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=functioLine truncated
|
||||
|
||||
/*! Respond.js v1.4.2: min/max-width media query polyfill
|
||||
* Copyright 2014 Scott Jehl
|
||||
* Licensed under MIT
|
||||
* http://j.mp/respondjs */
|
||||
|
||||
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){v(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},g=function(a){return a.replace(c.regex.minmaxwh,"").match(c.regex.other)};if(c.ajax=f,c.queue=d,c.unsupportedmq=g,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,comments:/\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,maxw:/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,minmaxwh:/\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,other:/\([^\)]*\)/g},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var h,i,j,k=a.document,l=k.documentElement,m=[],n=[],o=[],p={},q=30,r=k.getElementsByTagName("head")[0]||l,s=k.getElementsByTagName("base")[0],t=r.getElementsByTagName("link"),u=function(){var a,b=k.createElement("div"),c=k.body,d=l.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=k.createElement("body"),c.style.background="none"),l.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&l.insertBefore(c,l.firstChild),a=b.offsetWidth,f?l.removeChild(c):c.removeChild(b),l.style.fontSize=d,e&&(c.style.fontSize=e),a=j=parseFloat(a)},v=function(b){var c="clientWidth",d=l[c],e="CSS1Compat"===k.compatMode&&d||k.body[c]||d,f={},g=t[t.length-1],p=(new Date).getTime();if(b&&h&&q>p-h)return a.clearTimeout(i),i=a.setTimeout(v,q),void 0;h=p;for(var s in m)if(m.hasOwnProperty(s)){var w=m[s],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?j||u():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?j||u():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(n[w.rules]))}for(var C in o)o.hasOwnProperty(C)&&o[C]&&o[C].parentNode===r&&r.removeChild(o[C]);o.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=k.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,r.insertBefore(E,g.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(k.createTextNode(F)),o.push(E)}},w=function(a,b,d){var e=a.replace(c.regex.comments,"").replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},i=!f&&d;b.length&&(b+="/"),i&&(f=1);for(var j=0;f>j;j++){var k,l,o,p;i?(k=d,n.push(h(a))):(k=e[j].match(c.regex.findStyles)&&RegExp.$1,n.push(RegExp.$2&&h(RegExp.$2))),o=k.split(","),p=o.length;for(var q=0;p>q;q++)l=o[q],g(l)||m.push({media:l.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:n.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}v()},x=function(){if(d.length){var b=d.shift();f(b.href,function(c){w(c,b.href,b.media),p[b.href]=!0,a.setTimeout(function(){x()},0)})}},y=function(){for(var b=0;b<t.length;b++){var c=t[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!p[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(w(c.styleSheet.rawCssText,e,f),p[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!s||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}x()};y(),c.update=y,c.getEmValue=u,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Password</div>
|
||||
<div class="panel-body">
|
||||
<a href='{{ url_for('security.change_password') }}' class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('security.change_password') }}" id="change_password" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change
|
||||
</a>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Vehicles</div>
|
||||
<div class="panel-body">
|
||||
<a href="{{ url_for('create_vehicle') }}" class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('create_vehicle') }}" id="create_vehicle" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
|
||||
</a>
|
||||
</div>
|
||||
@@ -33,18 +33,28 @@
|
||||
{% for vehicle in current_user.vehicles %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ vehicle.name }}
|
||||
{{ vehicle.name }}<br />
|
||||
{% if not vehicle.is_active %}
|
||||
(inactive)
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ vehicle.pitstops | length }} pitstops<br />
|
||||
{{ vehicle.services | length }} special expenses<br />
|
||||
{{ vehicle.consumables | length }} consumables
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('create_service_for_vehicle', vid=vehicle.id) }}" id="pitstop_{{loop.index}}" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> add service
|
||||
</a>
|
||||
<a href="{{ url_for('select_consumable_for_new_pitstop', vid=vehicle.id) }}" id="pitstop_{{loop.index}}" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> add pitstop
|
||||
</a>
|
||||
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" id="edit_vehicle_{{loop.index}}" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
{% if current_user.vehicles | length > 1 %}
|
||||
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" class="btn btn-primary btn-warning " role="button">
|
||||
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" id="delete_vehicle_{{loop.index}}" class="btn btn-primary btn-warning " role="button">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -59,7 +69,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Account</div>
|
||||
<div class="panel-body">
|
||||
<a href='{{ url_for('delete_account') }}' class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('delete_account') }}" id="delete_account" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Consumables</div>
|
||||
<div class="panel-body">
|
||||
<a href="{{ url_for('create_consumable') }}" class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('create_consumable') }}" id="create_consumable" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
|
||||
</a>
|
||||
</div>
|
||||
@@ -38,22 +38,16 @@
|
||||
</tr>
|
||||
{% for consumable in consumables %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ consumable.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ consumable.unit }}
|
||||
</td>
|
||||
<td>
|
||||
{{ consumable.vehicles | length }} vehicles
|
||||
</td>
|
||||
<td id="name_{{loop.index}}">{{ consumable.name }}</td>
|
||||
<td id="unit_{{loop.index}}">{{ consumable.unit }}</td>
|
||||
<td id="count_{{loop.index}}">{{ consumable.vehicles | length }} vehicles</td>
|
||||
<td>
|
||||
{% if not consumable.in_use %}
|
||||
<a href="{{ url_for('delete_consumable', cid=consumable.id) }}" class="btn btn-primary btn-warning " role="button">
|
||||
<a href="{{ url_for('delete_consumable', cid=consumable.id) }}" id="delete_consumable{{loop.index}}" class="btn btn-primary btn-warning " role="button">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('edit_consumable', cid=consumable.id) }}" class="btn btn-primary " role="button">
|
||||
<a href="{{ url_for('edit_consumable', cid=consumable.id) }}" id="edit_consumable{{loop.index}}" class="btn btn-primary " role="button">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
{{messages['odometer']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.litres) }}
|
||||
<span id="{{form.litres.id}}_help" class="help-block">
|
||||
{{messages['litres']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>New Regular Cost for '{{ vehicle.name }}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ render_field_with_errors(form.start_at) }}
|
||||
<span id="{{form.start_at.id}}_help" class="help-block">
|
||||
{{messages['start_at']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.ends_at) }}
|
||||
<span id="{{form.ends_at.id}}_help" class="help-block">
|
||||
{{messages['ends_at']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.days) }}
|
||||
<span>Format as 'Month-Day' (e.g. 05-25) and separate with ','.</span>
|
||||
<span id="{{form.days.id}}_help" class="help-block">
|
||||
{{messages['days']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.description) }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2" ></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>New Service for '{{ vehicle.name }}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.date) }}
|
||||
<span id="{{form.date.id}}_help" class="help-block">
|
||||
{{messages['date']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.odometer) }}
|
||||
<span id="{{form.odometer.id}}_help" class="help-block">
|
||||
{{messages['odometer']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.description) }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2" ></div>
|
||||
{% endblock %}
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Delete vehicle '{{consumable.name}}'?</h3>
|
||||
<h3>Delete consumable '{{consumable.name}}'?</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel-body'>
|
||||
<h3>Delete pitstop?</h3>
|
||||
<table style='width: 100%'>
|
||||
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th style='text-align:right'>Date of Pitstop</th>
|
||||
<td style='text-align: left'>{{ pitstop.date }}</td>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends 'layout.html' %}
|
||||
|
||||
{% block body %}
|
||||
<div class='col-md-2' ></div>
|
||||
<div class='col-md-8'>
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel-body'>
|
||||
<h3>Delete Regular Cost?</h3>
|
||||
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th style='text-align:right'>Description of regular cost</th>
|
||||
<td style='text-align: left'>{{ regular_cost.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>Costs (per instance)</th>
|
||||
<td style='text-align: left'>
|
||||
{% if regular_cost.costs %}
|
||||
{{ regular_cost.costs }}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
€
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>Days for regular costs</th>
|
||||
<td style='text-align: left'>{{ regular_cost.days }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>regular costs starting from</th>
|
||||
<td style='text-align: left'>{{ regular_cost.start_at }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>regular costs ending at</th>
|
||||
<td style='text-align: left'>{{ regular_cost.ends_at }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<form class='form-horizontal' method='POST'>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-md-2' ></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'layout.html' %}
|
||||
|
||||
{% block body %}
|
||||
<div class='col-md-2' ></div>
|
||||
<div class='col-md-8'>
|
||||
<div class='panel panel-default'>
|
||||
<div class='panel-body'>
|
||||
<h3>Delete service?</h3>
|
||||
<table style='width: 100%' class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th style='text-align:right'>Date of Pitstop</th>
|
||||
<td style='text-align: left'>{{ service.date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>Odometer</th>
|
||||
<td style='text-align: left'>{{ service.odometer }} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>Description</th>
|
||||
<td style='text-align: left' class="markdown">{{ service.description | markdown | safe}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style='text-align:right'>Costs (overall)</th>
|
||||
<td style='text-align: left'>
|
||||
{% if service.costs %}
|
||||
{{service.costs}}
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
€
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<form class='form-horizontal' method='POST'>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='col-md-2' ></div>
|
||||
{% endblock %}
|
||||
@@ -8,7 +8,7 @@
|
||||
<h3>Delete vehicle '{{vehicle.name}}'?</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
{{ render_field_with_errors(form.submit, include_cancel=True) }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Edit consumable </h3>
|
||||
<h3>Edit consumable</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Edit Regular Cost for '{{ vehicle.name }}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ render_field_with_errors(form.start_at) }}
|
||||
<span id="{{form.start_at.id}}_help" class="help-block">
|
||||
{{messages['start_at']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.ends_at) }}
|
||||
<span id="{{form.ends_at.id}}_help" class="help-block">
|
||||
{{messages['ends_at']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.days) }}
|
||||
<span>Format as 'Month-Day' (e.g. 05-25) and separate with ','.</span>
|
||||
<span id="{{form.days.id}}_help" class="help-block">
|
||||
{{messages['days']}}
|
||||
</span>
|
||||
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.description) }}
|
||||
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Edit Pitstop for '{{ vehicle.name }}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.date) }}
|
||||
<span id="{{form.date.id}}_help" class="help-block">
|
||||
{{messages['date']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.odometer) }}
|
||||
<span id="{{form.odometer.id}}_help" class="help-block">
|
||||
{{messages['odometer']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.description) }}
|
||||
{{ render_field_with_errors(form.costs) }}
|
||||
<span id="{{form.costs.id}}_help" class="help-block">
|
||||
{{messages['costs']}}
|
||||
</span>
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,11 +5,12 @@
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Edit vehicle</h3>
|
||||
<h3>Edit vehicle '{{vehicle.name}}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
{{ render_field_with_errors(form.consumables) }}
|
||||
{{ render_field_with_errors(form.is_active) }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h3>Edit Regular Cost for '{{ vehicle.name }}'</h3>
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+67
-35
@@ -1,32 +1,31 @@
|
||||
{% 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>
|
||||
<li><a id='new_pitstop_link' href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li>
|
||||
<li><a id='new_service_link' href='{{ url_for('select_vehicle_for_new_service') }}'>Create Service</a></li>
|
||||
<li><a id='new_service_link' href='{{ url_for('select_vehicle_for_new_regular_cost') }}'>Create Regular Cost</a></li>
|
||||
<li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li>
|
||||
<li><a id='account_link' href='{{ url_for('get_account_page') }}'>Account</a></li>
|
||||
{% if current_user.has_role('admin') %}
|
||||
<li><a href='{{ url_for('get_admin_page') }}'>Admin</a></li>
|
||||
<li><a id='admin_link' href='{{ url_for('get_admin_page') }}'>Admin</a></li>
|
||||
{% endif %}
|
||||
<li><a href='{{ url_for('security.logout') }}'>Logout</a></li>
|
||||
<li><a id='logout_link' 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>
|
||||
<li><a id='login_link' href='{{ url_for('security.login') }}'>Login</a></li>
|
||||
<li><a id='register_link' href='{{ url_for('security.register') }}'>Register</a></li>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_field_with_errors(field) %}
|
||||
{% macro render_field_with_errors(field, include_cancel=True) %}
|
||||
<div class="form-group">
|
||||
{% if field.type == 'SubmitField' %}
|
||||
<div class="col-md-4" ></div>
|
||||
|
||||
<div class="col-sm-4" style="align:center">
|
||||
<div class="col-md-3" ></div>
|
||||
<div class="col-sm-6" style="align:center">
|
||||
{% if include_cancel %}
|
||||
<input id="{{ field.id }}_cancel" name="{{ field.id }}_cancel" class="btn btn-default" type="submit" value="Cancel" onclick="window.history.go(-1)">
|
||||
{% endif %}
|
||||
<input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}">
|
||||
</div>
|
||||
<!--
|
||||
<div class="col-sm-3" style="align:center">
|
||||
<a class="btn btn-default" href="{{ g.data['back'] }}" role="button">Cancel</a>
|
||||
</div>
|
||||
-->
|
||||
<div class="col-md-4" ></div>
|
||||
<div class="col-md-3" ></div>
|
||||
{% else %}
|
||||
<label class="col-sm-6 control-label">
|
||||
{{ field.label }}
|
||||
@@ -45,7 +44,7 @@
|
||||
{% 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" />
|
||||
<input class="form-control" type="checkbox" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.id }}" aria-describedby="{{ field.id }}_help" {% if field.default %}checked{% endif %}/>
|
||||
{% elif field.type == 'StringField' %}
|
||||
<input class="form-control" type="text" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" aria-describedby="{{ field.id }}_help" />
|
||||
{% elif field.type == 'PasswordField' %}
|
||||
@@ -56,6 +55,8 @@
|
||||
<input class="form-control" type="number" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" step="1" aria-describedby="{{ field.id }}_help" />
|
||||
{% elif field.type == 'DecimalField' %}
|
||||
<input class="form-control" type="number" id="{{ field.id }}" name="{{ field.id }}" value="{{ field.default|none_filter }}" step="{{ 1 / 10 ** field.places}}" aria-describedby="{{ field.id }}_help" />
|
||||
{% elif field.type == 'TextAreaField' %}
|
||||
<textarea class="form-control" id="{{ field.id }}" name="{{ field.id }}">{{ field.default|none_filter }}</textarea>
|
||||
{% else %}
|
||||
{{ field(**kwargs)|safe }}
|
||||
{% endif %}
|
||||
@@ -103,19 +104,19 @@
|
||||
<title>refuel journal</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-57.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-60.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-72.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-76.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-114.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-120.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-144.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-152.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-180.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="{{ url_for('static', filename='android-icon-192x192.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='favicon-96x96.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='favicon-16x16.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-57.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-60.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-72.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-76.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-114.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-120.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-144.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-152.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='img/apple-touch-icon-180.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="{{ url_for('static', filename='img/android-icon-192x192.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='img/favicon-32x32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ url_for('static', filename='img/favicon-96x96.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='img/favicon-16x16.png') }}">
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
|
||||
@@ -124,13 +125,16 @@
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/jquery.tablesorter.min.js') }}"></script>
|
||||
|
||||
<!-- Latest compiled and minified JavaScript -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
<script src="https://www.amcharts.com/lib/3/amcharts.js"></script>
|
||||
<script src="https://www.amcharts.com/lib/3/serial.js"></script>
|
||||
<script src="https://www.amcharts.com/lib/3/themes/patterns.js"></script>
|
||||
<script src="{{ url_for('static', filename='main.js') }}"></script>
|
||||
<script src="https://openlayers.org/api/OpenLayers.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
@@ -151,13 +155,41 @@
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="row topspace">
|
||||
<div class="col-md-4" ></div>
|
||||
<div class="col-md-4">
|
||||
<div class="alert alert-{{category}} alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4" ></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="container topspace">
|
||||
<div class="starter-template">
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container topspace">
|
||||
<div class="starter-template">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<a href="https://lusiardi.de/pages/impressum.html" target="_new">Impressum</a> - <a href="https://lusiardi.de/pages/datenschutzerklaerung.html" target="_new">Datenschutzerklärung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#
|
||||
<nav class="navbar navbar-inverse navbar-fixed-bottom">
|
||||
<div class="container">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% macro line(header, cell) -%}
|
||||
<tr>
|
||||
<th>
|
||||
{{ header }}
|
||||
</th>
|
||||
<td>
|
||||
{{ cell }}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endmacro %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
{{ line('Reifenluftdruck (vorne)', '1,75 - 2,0 Bar') }}
|
||||
{{ line('Reifenluftdruck (hinten)', '2,0 - 2,25 Bar') }}
|
||||
{{ line('Scheinwerfer', '12V 35/35W HS1-Halogen-Glühlampe') }}
|
||||
{{ line('Standlicht', '12V 5W Glassockel 9mm') }}
|
||||
{{ line('Blinker vorne', '12V 10W Stecksockel 15mm') }}
|
||||
{{ line('Rück-/Bremslicht', '12V/21/5W Stecksockel 15mm') }}
|
||||
{{ line('Blinker hinten', '12V 10W Stecksockel 15mm') }}
|
||||
{{ line('Tankinhalt', 'ca. 6,0 L') }}
|
||||
{{ line('Motoröl', 'SAE 15W40') }}
|
||||
{{ line('Getriebeöl', 'SAE 80/90 (0,12/0,09)') }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
+195
-41
@@ -1,9 +1,182 @@
|
||||
{% extends "layout.html" %}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% macro regular(field, vindex, loop) -%}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div style="text-align: left; font-size: 20px;">
|
||||
<span class="glyphicon glyphicon-repeat" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;" />
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.description}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Costs</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
|
||||
{% if field.costs %}
|
||||
{{field.costs}} €
|
||||
{% else %}
|
||||
-- €
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>From</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.start_at}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>To</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.ends_at}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Days</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.days}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if loop.first %}
|
||||
{% endif %}
|
||||
<a id="vehicle_{{vindex}}_edit_regular_{{loop.index}}" href="{{ url_for('edit_regular_form', pid=field.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
<a id="vehicle_{{vindex}}_delete_regular_{{loop.index}}" href="{{ url_for('delete_regular_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro regular_instance(field, vindex, loop) -%}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div style="text-align: left; font-size: 20px;">
|
||||
<span class="glyphicon glyphicon-repeat" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Costs</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
|
||||
{% if field.costs %}
|
||||
{{field.costs}} €
|
||||
{% else %}
|
||||
-- €
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if loop.first %}
|
||||
{% endif %}
|
||||
<a id="vehicle_{{vindex}}_edit_regular_{{loop.index}}" href="{{ url_for('edit_regular_form', pid=field.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
<a id="vehicle_{{vindex}}_end_regular_{{loop.index}}" href="{{ url_for('end_regular_form', pid=field.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span> end series
|
||||
</a>
|
||||
<a id="vehicle_{{vindex}}_delete_regular_{{loop.index}}" href="{{ url_for('delete_regular_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro pitstop(field, vindex, loop) -%}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div style="text-align: left; font-size: 20px;">
|
||||
<span class="glyphicon glyphicon-filter" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Odometer</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_odo">{{field.odometer}} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ field.consumable.name }}</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_anmount">{{field.amount}} {{ field.consumable.unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Costs</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
|
||||
{% if field.costs %}
|
||||
{{field.costs}} €
|
||||
{% else %}
|
||||
-- €
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if loop.first %}
|
||||
{% endif %}
|
||||
<a id="vehicle_{{vindex}}_edit_pitstop_{{loop.index}}" href="{{ url_for('edit_pit_stop_form', pid=field.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
<a id="vehicle_{{vindex}}_delete_pitstop_{{loop.index}}" href="{{ url_for('delete_pit_stop_form', pid=field.id) }}" class="btn btn-primary btn-warning ">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro service(field, vindex, loop) -%}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div style="text-align: left; font-size: 20px;">
|
||||
<span class="glyphicon glyphicon-wrench" aria-hidden="true" style="border: 1px solid black; padding: 5px 5px 3px; border-radius: 5px;"></span>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_date">{{field.date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Odometer</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_desc">{{field.odometer}} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_anmount" class="markdown">{{field.description | markdown | safe}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Costs</th>
|
||||
<td id="vehicle_{{vindex}}_pitstop_{{loop.index}}_cost">
|
||||
{% if field.costs %}
|
||||
{{field.costs}} €
|
||||
{% else %}
|
||||
-- €
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if loop.first %}
|
||||
{% endif %}
|
||||
<a id="vehicle_{{vindex}}_edit_pitstop_{{loop.index}}" href="{{ url_for('edit_service_form', sid=field.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
<a id="vehicle_{{vindex}}_delete_pitstop_{{loop.index}}" href="{{ url_for('delete_service_form', sid=field.id) }}" class="btn btn-primary btn-warning ">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-2" ></div>
|
||||
<div class="col-md-8">
|
||||
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
|
||||
{% for vehicle in current_user.vehicles %}
|
||||
{% for vehicle in user.vehicles %}
|
||||
<li {% if loop.first %}class="active" {%endif %}>
|
||||
<a href="#v{{vehicle.id}}" id="i{{vehicle.id}}" data-toggle="tab">
|
||||
{{ vehicle.name }}
|
||||
@@ -12,53 +185,34 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div id="my-tab-content" class="tab-content">
|
||||
{% for vehicle in current_user.vehicles %}
|
||||
{% for vehicle in user.vehicles %}
|
||||
{% set vehicleloop = loop %}
|
||||
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
|
||||
<h3>{{vehicle.name}}</h3>
|
||||
{% if vehicle.pitstops %}
|
||||
{% for pitstop in vehicle.pitstops|reverse %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped table-bordered table-condensed">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td>{{pitstop.date}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Odometer</th>
|
||||
<td>{{pitstop.odometer}} km</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ pitstop.consumable.name }}</th>
|
||||
<td>{{pitstop.amount}} {{ pitstop.consumable.unit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Costs</th>
|
||||
<td>
|
||||
{% if pitstop.costs %}
|
||||
{{pitstop.costs}} €
|
||||
{% else %}
|
||||
-- €
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% if loop.first %}
|
||||
<a href="{{ url_for('edit_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary">
|
||||
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
|
||||
</a>
|
||||
<a href="{{ url_for('delete_pit_stop_form', pid=pitstop.id) }}" class="btn btn-primary btn-warning ">
|
||||
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if vehicle.data %}
|
||||
{% for data in vehicle.data|reverse %}
|
||||
{% if 'Pitstop' in data.__class__.__name__ %}
|
||||
{{ pitstop(data, vehicleloop.index, loop) }}
|
||||
{% endif %}
|
||||
{% if 'Service' in data.__class__.__name__ %}
|
||||
{{ service(data, vehicleloop.index, loop) }}
|
||||
{% endif %}
|
||||
{% if 'Regular' in data.__class__.__name__ %}
|
||||
{{ regular_instance(data, vehicleloop.index, loop) }}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
not enough data: <a href="{{ url_for('select_consumable_for_new_pitstop', vid=vehicle.id) }}">log a pitstop</a>?
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if vehicle.regulars %}
|
||||
<h4>Regular Costs</h4>
|
||||
{% for data in vehicle.regulars %}
|
||||
{{ regular(data, vehicleloop.index, loop) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,14 @@
|
||||
|
||||
{% macro nav_tab(id, text, active) %}
|
||||
{#
|
||||
Create a UI element to select the shown pane of a tabbed view
|
||||
|
||||
id:
|
||||
id of the pane to select
|
||||
text:
|
||||
the text in the UI element
|
||||
active:
|
||||
boolean stating if the tab is active or not
|
||||
#}
|
||||
<li class="{% if active %}active{% endif %}">
|
||||
<a href="#ref_{{id}}" id="id_{{id}}" data-toggle="tab" >
|
||||
@@ -87,7 +94,15 @@
|
||||
<th>Logged Costs:</th>
|
||||
<td>{{ vehicle.overall_costs | round(2) }} €</td>
|
||||
</tr>
|
||||
</table>
|
||||
<tr>
|
||||
<th>Logged Costs per km:</th>
|
||||
{% if vehicle.costs_per_distance != 'N/A' %}
|
||||
<td>{{ vehicle.costs_per_distance | round(2) }} €/100km</td>
|
||||
{% else %}
|
||||
<td>{{ vehicle.costs_per_distance }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -115,6 +130,7 @@
|
||||
{{ print_vehicle_table(vehicle) }}
|
||||
<ul id="vehicle_{{vehicle.id}}_tabs" class="nav nav-tabs" data-tabs="tabs">
|
||||
{{ nav_tab(vehicle.id|string + '_odometer', 'Odometer', true) }}
|
||||
{{ nav_tab(vehicle.id|string + '_costs', 'Costs', false) }}
|
||||
{% for consumable in vehicle.consumables %}
|
||||
{{ nav_tab(vehicle.id|string + '_' + consumable.id|string, consumable.name, false) }}
|
||||
{% endfor %}
|
||||
@@ -131,12 +147,24 @@
|
||||
true
|
||||
)
|
||||
}}
|
||||
{{ tab_pane(
|
||||
vehicle.id|string + '_costs',
|
||||
chart(
|
||||
vehicle.costs,
|
||||
'ref_' + vehicle.id|string + '_costs',
|
||||
'€',
|
||||
url_for('select_consumable_for_new_pitstop', vid=vehicle.id)
|
||||
),
|
||||
false
|
||||
)
|
||||
}}
|
||||
{% for consumable in vehicle.consumables %}
|
||||
<div class="tab-pane" id="ref_{{vehicle.id}}_{{consumable.id}}">
|
||||
{{ print_consumable_table(consumable) }}
|
||||
<ul id="consumable_{{vehicle.id}}_{{consumable.id}}_tabs" class="nav nav-tabs" data-tabs="tabs">
|
||||
{{ nav_tab(vehicle.id|string + '_' + consumable.id|string + '_consumption', 'Consumption', true) }}
|
||||
{{ nav_tab(vehicle.id|string + '_' + consumable.id|string + '_amount', 'Amount', false) }}
|
||||
{{ nav_tab(vehicle.id|string + '_' + consumable.id|string + '_price', 'Price', false) }}
|
||||
</ul>
|
||||
<div id="consumable_{{vehicle.id}}_{{consumable.id}}_content" class="tab-content ">
|
||||
{{ tab_pane(
|
||||
@@ -161,6 +189,17 @@
|
||||
false
|
||||
)
|
||||
}}
|
||||
{{ tab_pane(
|
||||
vehicle.id|string + '_' + consumable.id|string + '_price',
|
||||
chart(
|
||||
consumable.price,
|
||||
'ref_' + vehicle.id|string + '_' + consumable.id|string + '_price',
|
||||
'€ / '+consumable.unit,
|
||||
url_for('create_pit_stop_form', vid=vehicle.id, cid=consumable.id)
|
||||
),
|
||||
false
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
{{ tab_script('vehicle_' + vehicle.id|string + '_' + consumable.id|string + '_tabs') }}
|
||||
</div>
|
||||
|
||||
+139
-46
@@ -1,7 +1,19 @@
|
||||
from sqlalchemy import or_
|
||||
import requests
|
||||
import logging
|
||||
from datetime import date
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from .entities import Pitstop
|
||||
from . import db, app
|
||||
|
||||
|
||||
class RegularCostInstance:
|
||||
def __init__(self, regular_id, date, odometer, costs, name):
|
||||
self.id = regular_id
|
||||
self.date = date
|
||||
self.odometer = odometer
|
||||
self.costs = costs
|
||||
self.name = name
|
||||
|
||||
|
||||
class ConsumableStats:
|
||||
@@ -15,26 +27,43 @@ class ConsumableStats:
|
||||
self.average_amount_used = 0
|
||||
self.average_amount = []
|
||||
self.amounts = []
|
||||
self.price = []
|
||||
|
||||
pitstops = [stop for stop in vehicle.pitstops if stop.consumable_id == consumable.id]
|
||||
pitstops = [
|
||||
stop for stop in vehicle.pitstops if stop.consumable_id == consumable.id
|
||||
]
|
||||
pitstop_count = len(pitstops)
|
||||
|
||||
if pitstop_count > 0:
|
||||
for pitstop in pitstops:
|
||||
self.overall_amount += pitstop.amount
|
||||
self.amounts.append(StatsEvent(pitstop.date, pitstop.amount))
|
||||
# some pitstops seem to have lost their costs...
|
||||
if pitstop.costs:
|
||||
self.price.append(StatsEvent(pitstop.date, pitstop.costs/pitstop.amount))
|
||||
self.average_amount_fuelled = self.overall_amount / pitstop_count
|
||||
if pitstop_count > 1:
|
||||
overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
||||
overall_distance = (
|
||||
vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
||||
)
|
||||
self.average_distance = overall_distance / (pitstop_count - 1)
|
||||
self.average_amount_used = 100 * (self.overall_amount - pitstops[0].amount) / overall_distance
|
||||
self.average_amount_used = (
|
||||
100 * (self.overall_amount - pitstops[0].amount) / overall_distance
|
||||
)
|
||||
for index in range(1, pitstop_count):
|
||||
last_ps = pitstops[index - 1]
|
||||
current_ps = pitstops[index]
|
||||
self.average_amount.append(
|
||||
StatsEvent(
|
||||
current_ps.date,
|
||||
round(100 * current_ps.amount/(current_ps.odometer - last_ps.odometer), 2)))
|
||||
round(
|
||||
100
|
||||
* current_ps.amount
|
||||
/ (current_ps.odometer - last_ps.odometer),
|
||||
2,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VehicleStats:
|
||||
@@ -45,20 +74,42 @@ class VehicleStats:
|
||||
self.overall_costs = 0
|
||||
self.consumables = []
|
||||
self.odometers = []
|
||||
self.costs = []
|
||||
self.costs_per_distance = "N/A"
|
||||
|
||||
for consumable in vehicle.consumables:
|
||||
self.consumables.append(ConsumableStats(vehicle, consumable))
|
||||
|
||||
pitstop_count = len(vehicle.pitstops)
|
||||
events = get_event_line_for_vehicle(vehicle)
|
||||
pitstop_count = len(events)
|
||||
|
||||
if pitstop_count > 0:
|
||||
for pitstop in vehicle.pitstops:
|
||||
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
|
||||
if pitstop.costs is not None:
|
||||
for pitstop in events:
|
||||
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
|
||||
if pitstop.costs is not None:
|
||||
self.overall_costs += pitstop.costs
|
||||
self.costs.append(StatsEvent(pitstop.date, pitstop.costs))
|
||||
|
||||
# add the instances to the overall costs
|
||||
for regular_cost_instance in calculate_regular_cost_instances(vehicle):
|
||||
self.overall_costs += regular_cost_instance.costs
|
||||
self.costs.append(
|
||||
StatsEvent(regular_cost_instance.date, regular_cost_instance.costs)
|
||||
)
|
||||
|
||||
if pitstop_count > 1:
|
||||
self.overall_distance = vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
||||
self.overall_distance = events[-1].odometer - events[0].odometer
|
||||
|
||||
self.costs.sort(key=lambda x: x.date)
|
||||
accumulated_costs = 0
|
||||
for c in self.costs:
|
||||
accumulated_costs += c.value
|
||||
c.value = accumulated_costs
|
||||
|
||||
if self.overall_distance > 0:
|
||||
self.costs_per_distance = float(self.overall_costs) / (
|
||||
float(self.overall_distance) / 100
|
||||
)
|
||||
|
||||
|
||||
class StatsEvent:
|
||||
@@ -66,17 +117,20 @@ class StatsEvent:
|
||||
self.date = date
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.date)
|
||||
|
||||
|
||||
def db_log_add(entity):
|
||||
logging.info('db_add: %s' % str(entity))
|
||||
logging.info("db_add: %s" % str(entity))
|
||||
|
||||
|
||||
def db_log_delete(entity):
|
||||
logging.info('db_delete: %s' % str(entity))
|
||||
logging.info("db_delete: %s" % str(entity))
|
||||
|
||||
|
||||
def db_log_update(entity):
|
||||
logging.info('db_update: %s' % str(entity))
|
||||
logging.info("db_update: %s" % str(entity))
|
||||
|
||||
|
||||
def check_vehicle_name_is_unique(current_user, name_field):
|
||||
@@ -103,10 +157,11 @@ def get_latest_pitstop_for_vehicle(vehicle_id):
|
||||
:param vehicle_id: the id of the vehicle
|
||||
:return: the latest pitstop or None if no pitstop exists
|
||||
"""
|
||||
latest_pitstop = Pitstop.query\
|
||||
.filter(Pitstop.vehicle_id == vehicle_id)\
|
||||
.order_by(Pitstop.id.desc())\
|
||||
latest_pitstop = (
|
||||
Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
|
||||
.order_by(Pitstop.id.desc())
|
||||
.first()
|
||||
)
|
||||
return latest_pitstop
|
||||
|
||||
|
||||
@@ -117,15 +172,18 @@ def get_latest_pitstop_for_vehicle_and_consumable(vehicle_id, consumable_id):
|
||||
:param consumable_id: the id of the consumable
|
||||
:return: the latest pitstop or None if no pitstop exists
|
||||
"""
|
||||
latest_pitstop_consumable = Pitstop.query\
|
||||
.filter(Pitstop.vehicle_id == vehicle_id)\
|
||||
.filter(Pitstop.consumable_id == consumable_id)\
|
||||
.order_by(Pitstop.id.desc())\
|
||||
latest_pitstop_consumable = (
|
||||
Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
|
||||
.filter(Pitstop.consumable_id == consumable_id)
|
||||
.order_by(Pitstop.id.desc())
|
||||
.first()
|
||||
)
|
||||
return latest_pitstop_consumable
|
||||
|
||||
|
||||
def compute_lower_limits_for_new_pitstop(latest_pitstop, last_pitstop_consumable, consumable_id):
|
||||
def compute_lower_limits_for_new_pitstop(
|
||||
latest_pitstop, last_pitstop_consumable, consumable_id
|
||||
):
|
||||
"""
|
||||
This function figures out the lower limits for date and odometer of a new pitstop.
|
||||
:param latest_pitstop:
|
||||
@@ -149,29 +207,64 @@ def compute_lower_limits_for_new_pitstop(latest_pitstop, last_pitstop_consumable
|
||||
return Pitstop(odometer, amount, date_of_pitstop, costs, consumable_id)
|
||||
|
||||
|
||||
# if latest_pitstop is not None:
|
||||
# if last_pitstop_consumable is not None and last_pitstop_consumable != latest_pitstop:
|
||||
# if latest_pitstop.id > last_pitstop_consumable.id:
|
||||
# return Pitstop(latest_pitstop.odometer,
|
||||
# last_pitstop_consumable.overall_amount,
|
||||
# latest_pitstop.date,
|
||||
# last_pitstop_consumable.costs,
|
||||
# consumable_id)
|
||||
# else:
|
||||
# return Pitstop(last_pitstop_consumable.odometer,
|
||||
# last_pitstop_consumable.overall_amount,
|
||||
# last_pitstop_consumable.date,
|
||||
# last_pitstop_consumable.costs,
|
||||
# consumable_id)
|
||||
# else:
|
||||
# # either only one pitstop exists or both are the same
|
||||
# litres = 0
|
||||
# costs = 0
|
||||
# if latest_pitstop.consumable_id == last_pitstop_consumable.consumable_id:
|
||||
# litres = latest_pitstop.overall_amount
|
||||
# costs = latest_pitstop.costs
|
||||
# return Pitstop(latest_pitstop.odometer, litres, latest_pitstop.date, costs, consumable_id)
|
||||
# else:
|
||||
# # No existing pitstop at all: insert fake data
|
||||
# return Pitstop(0, 0, date(1970, 1, 1), 0, None)
|
||||
def pitstop_service_key(x):
|
||||
# if the entry got no odometer (regular costs!) then we assume it's okay
|
||||
# to have regular cost at a virtual odometer of 0 on that day.
|
||||
return x.date, x.odometer or 0
|
||||
|
||||
|
||||
def get_event_line_for_vehicle(vehicle):
|
||||
data = []
|
||||
for pitstop in vehicle.pitstops:
|
||||
data.append(pitstop)
|
||||
for service in vehicle.services:
|
||||
data.append(service)
|
||||
data.sort(key=pitstop_service_key)
|
||||
return data
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i : i + n]
|
||||
|
||||
|
||||
def calculate_regular_cost_instances(vehicle):
|
||||
data = []
|
||||
for regular in vehicle.regulars:
|
||||
if date.today() < regular.start_at:
|
||||
# skip regular costs that are not yet active
|
||||
continue
|
||||
if regular.ends_at:
|
||||
end_date = regular.ends_at
|
||||
else:
|
||||
end_date = date.today()
|
||||
|
||||
start_year = regular.start_at.year
|
||||
end_year = end_date.year
|
||||
|
||||
for year in range(start_year, end_year + 1):
|
||||
for day in regular.days.split(","):
|
||||
m, d = day.split("-")
|
||||
d = date(year, int(m), int(d))
|
||||
if regular.start_at <= d and d <= end_date:
|
||||
r = RegularCostInstance(
|
||||
regular.id,
|
||||
date=d,
|
||||
odometer=None,
|
||||
costs=regular.costs,
|
||||
name=regular.description,
|
||||
)
|
||||
data.append(r)
|
||||
return data
|
||||
|
||||
|
||||
def get_users_active_vehicle(user):
|
||||
def selector(vehicle):
|
||||
if not vehicle.pitstops:
|
||||
return date.today()
|
||||
return vehicle.pitstops[-1].date
|
||||
|
||||
active_vehicles = [g for g in user.vehicles if g.is_active]
|
||||
active_vehicles.sort(key=selector, reverse=True)
|
||||
return active_vehicles
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
@@ -17,6 +18,9 @@ class Config:
|
||||
MAIL_USE_SSL = False
|
||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||
ADMIN_MAIL = 'joachim@lusiardi.de'
|
||||
TANKERKOENIG_API_KEY = os.environ.get('TANKERKOENIG_API_KEY')
|
||||
DEBUG = False
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
@@ -26,16 +30,19 @@ class Config:
|
||||
class DevelopmentConfig(Config):
|
||||
SECURITY_SEND_REGISTER_EMAIL = False
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD'])
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/data.sqlite'
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data_testing.sqlite')
|
||||
SECURITY_SEND_REGISTER_EMAIL = False
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:{p}@{h}/pitstops'.format(p=os.environ.get('MYSQL_PASSWORD'),
|
||||
h=os.environ.get('MYSQL_HOST'))
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://pitstops:{h}@localhost/pitstops'.format(
|
||||
h=os.environ.get('MYSQL_PASSWORD'))
|
||||
|
||||
|
||||
config = {
|
||||
@@ -43,4 +50,4 @@ config = {
|
||||
'testing': TestingConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS `service` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`date` date DEFAULT NULL,
|
||||
`odometer` int(11) DEFAULT NULL,
|
||||
`vehicle_id` int(11) DEFAULT NULL,
|
||||
`costs` decimal(10,2) DEFAULT NULL,
|
||||
`description` varchar(4096) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `vehicle_id` (`vehicle_id`),
|
||||
CONSTRAINT `service_ibfk_1` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicle` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=latin1;
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE TABLE `filling_station` (
|
||||
`int_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`id` varchar(40) NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`street` text NOT NULL,
|
||||
`place` text NOT NULL,
|
||||
`houseNumber` text,
|
||||
`postCode` int(11) NOT NULL,
|
||||
`brand` text NOT NULL,
|
||||
`lat` decimal(8,5) NOT NULL,
|
||||
`lng` decimal(8,5) NOT NULL,
|
||||
`last_update` datetime DEFAULT NULL,
|
||||
`diesel` decimal(10,3) DEFAULT NULL,
|
||||
`e5` decimal(10,3) DEFAULT NULL,
|
||||
`e10` decimal(10,3) DEFAULT NULL,
|
||||
`open` tinyint(1) DEFAULT NULL,
|
||||
PRIMARY KEY (`int_id`),
|
||||
UNIQUE KEY `id` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE `users_fillingstations` (
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`fillingstation_id` int(11) DEFAULT NULL,
|
||||
KEY `user_id` (`user_id`),
|
||||
KEY `fillingstation_id` (`fillingstation_id`),
|
||||
CONSTRAINT `users_fillingstations_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
|
||||
CONSTRAINT `users_fillingstations_ibfk_2` FOREIGN KEY (`fillingstation_id`) REFERENCES `filling_station` (`int_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
ALTER TABLE `consumable` ADD COLUMN `ext_id` varchar(255) DEFAULT NULL;
|
||||
update consumable set ext_id = 'e5' where id = 1;
|
||||
update consumable set ext_id = 'e10' where id = 34;
|
||||
update consumable set ext_id = 'diesel' where id = 32;
|
||||
|
||||
|
||||
ALTER TABLE `user` ADD COLUMN `home_lat` decimal(8,5) DEFAULT NULL;
|
||||
ALTER TABLE `user` ADD COLUMN `home_long` decimal(8,5) DEFAULT NULL;
|
||||
ALTER TABLE `user` ADD COLUMN `home_zoom` int(11) DEFAULT NULL;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `vehicle` ADD COLUMN `is_active` tinyint(1);
|
||||
UPDATE `vehicle` SET `is_active` = 1;
|
||||
+2
-12
@@ -2,19 +2,9 @@ version: '2'
|
||||
services:
|
||||
rollerverbrauch:
|
||||
build: .
|
||||
depends_on:
|
||||
- database
|
||||
volumes:
|
||||
- ./compose_config/:/config
|
||||
environment:
|
||||
- config=/config/config.py
|
||||
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
|
||||
- FLASK_CONFIG=testing
|
||||
ports:
|
||||
- 5000
|
||||
database:
|
||||
image: mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=foobar123
|
||||
- MYSQL_DATABASE=pitstops
|
||||
ports:
|
||||
- 3306
|
||||
- 127.0.0.1:5000:5000
|
||||
@@ -1,12 +1,17 @@
|
||||
import os
|
||||
from app import app
|
||||
import logging
|
||||
from config import config
|
||||
|
||||
|
||||
def setup_logging():
|
||||
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
|
||||
|
||||
if __name__ == '__main__':
|
||||
DEBUG = 'DEBUG' in os.environ and os.environ['DEBUG'] != 'False'
|
||||
c = config[os.getenv('FLASK_CONFIG') or 'default']
|
||||
|
||||
DEBUG = c.DEBUG
|
||||
DEBUG = os.environ.get('DEBUG', DEBUG)
|
||||
|
||||
setup_logging()
|
||||
app.run(debug=DEBUG, host='0.0.0.0')
|
||||
+13
-5
@@ -1,5 +1,13 @@
|
||||
Flask
|
||||
Flask-SQLAlchemy
|
||||
Flask-Security
|
||||
Flask-WTF
|
||||
PyMySQL
|
||||
Flask==2.1.2
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-Security==3.0.0
|
||||
Flask-WTF==1.0.1
|
||||
PyMySQL==1.0.2
|
||||
markdown
|
||||
Flask-Limiter==2.4.5.1
|
||||
requests==2.27.1
|
||||
email-validator==1.2.1
|
||||
gunicorn==20.1.0
|
||||
pytz==2022.1
|
||||
SQLAlchemy==1.4.36
|
||||
Werkzeug==2.2.2
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM debian8_python3
|
||||
|
||||
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,137 @@
|
||||
import unittest
|
||||
import inspect
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
|
||||
|
||||
def wait_net_service(server, port, timeout=None):
|
||||
""" Wait for network service to appear
|
||||
@param timeout: in seconds, if None or 0 wait forever
|
||||
@return: True of False, if timeout is None may return only True or
|
||||
throw unhandled network exception
|
||||
"""
|
||||
import socket
|
||||
import errno
|
||||
|
||||
s = socket.socket()
|
||||
if timeout:
|
||||
from time import time as now
|
||||
# time module is needed to calc timeout shared between two exceptions
|
||||
end = now() + timeout
|
||||
|
||||
while True:
|
||||
try:
|
||||
if timeout:
|
||||
next_timeout = end - now()
|
||||
if next_timeout < 0:
|
||||
return False
|
||||
else:
|
||||
s.settimeout(next_timeout)
|
||||
|
||||
s.connect((server, port))
|
||||
|
||||
except socket.timeout as err:
|
||||
# this exception occurs only if timeout is set
|
||||
if timeout:
|
||||
return False
|
||||
|
||||
except socket.error as err:
|
||||
# catch timeout exception from underlying network library
|
||||
# this one is different from socket.timeout
|
||||
if type(err.args) != tuple or err[0] != errno.ETIMEDOUT:
|
||||
raise
|
||||
else:
|
||||
s.close()
|
||||
return True
|
||||
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.screen_shot_counter = 0
|
||||
self.driver = webdriver.Remote(
|
||||
command_executor='http://selenium:4444/wd/hub',
|
||||
desired_capabilities=DesiredCapabilities.FIREFOX)
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "email")))
|
||||
|
||||
def create_screenshot(self):
|
||||
self.driver.get_screenshot_as_file(
|
||||
'/results/%s_%s_%s.png' % (self.__class__.__name__, inspect.stack()[1][3], str(self.screen_shot_counter)))
|
||||
self.screen_shot_counter += 1
|
||||
|
||||
def tearDown(self):
|
||||
self.driver.close()
|
||||
|
||||
|
||||
class RegisterCheck(BaseTestCase):
|
||||
def setUp(self):
|
||||
self.screen_shot_counter = 0
|
||||
self.driver = webdriver.Remote(
|
||||
command_executor='http://selenium:4444/wd/hub',
|
||||
desired_capabilities=DesiredCapabilities.FIREFOX)
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "email")))
|
||||
|
||||
def test_page_loads(self):
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
self.create_screenshot()
|
||||
self.assertIn("refuel", self.driver.title, "Title must contain reload")
|
||||
|
||||
def test_can_register(self):
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
self.driver.find_element_by_partial_link_text('Register').click()
|
||||
self.create_screenshot()
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('email').send_keys('test@test.com')
|
||||
self.driver.find_element_by_id('password').send_keys('test123')
|
||||
self.driver.find_element_by_id('password_confirm').send_keys('test123')
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('submit').click()
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "i1")))
|
||||
self.create_screenshot()
|
||||
|
||||
def test_register_must_repeat_pwd(self):
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
self.driver.find_element_by_partial_link_text('Register').click()
|
||||
self.create_screenshot()
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('email').send_keys('test1@test.com')
|
||||
self.driver.find_element_by_id('password').send_keys('test123')
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('submit').click()
|
||||
self.create_screenshot()
|
||||
error = self.driver.find_elements_by_class_name('error')
|
||||
self.assertIsNotNone(error[0], 'we expect an error')
|
||||
self.assertIn('Passwords do not match', error[0].text, 'wrong error message')
|
||||
|
||||
def test_register_must_be_equal_pwd(self):
|
||||
self.driver.get("http://rollerverbrauch:5000")
|
||||
self.driver.find_element_by_partial_link_text('Register').click()
|
||||
self.create_screenshot()
|
||||
WebDriverWait(self.driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "submit")))
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('email').send_keys('test1@test.com')
|
||||
self.driver.find_element_by_id('password').send_keys('test123')
|
||||
self.driver.find_element_by_id('password_confirm').send_keys('test1234')
|
||||
self.create_screenshot()
|
||||
self.driver.find_element_by_id('submit').click()
|
||||
self.create_screenshot()
|
||||
error = self.driver.find_elements_by_class_name('error')
|
||||
self.assertIsNotNone(error[0], 'we expect an error')
|
||||
self.assertIn('Passwords do not match', error[0].text, 'wrong error message')
|
||||
|
||||
|
||||
class LoginCheck(BaseTestCase):
|
||||
|
||||
def can_login(self):
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
wait_net_service('selenium', 4444)
|
||||
unittest.main()
|
||||
@@ -0,0 +1 @@
|
||||
selenium
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
|
||||
#SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD'])
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/rollerverbrauch.db'
|
||||
|
||||
MAIL_SERVER = ''
|
||||
MAIL_PORT= 25
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_USE_SSL = False
|
||||
MAIL_USERNAME = ''
|
||||
MAIL_PASSWORD = ''
|
||||
SECURITY_EMAIL_SENDER = ''
|
||||
SECURITY_PASSWORD_SALT = 'SecretSalt'
|
||||
SECRET_KEY = 'SecretKey'
|
||||
|
||||
SECURITY_SEND_REGISTER_EMAIL = False
|
||||
@@ -0,0 +1,22 @@
|
||||
version: '2'
|
||||
services:
|
||||
tests:
|
||||
build: .
|
||||
depends_on:
|
||||
- selenium
|
||||
volumes:
|
||||
- ./results:/results
|
||||
selenium:
|
||||
image: selenium/standalone-firefox
|
||||
depends_on:
|
||||
- rollerverbrauch
|
||||
rollerverbrauch:
|
||||
build: ..
|
||||
volumes:
|
||||
- ./compose_config/:/config
|
||||
environment:
|
||||
- config=/config/config.py
|
||||
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
|
||||
ports:
|
||||
- 5000
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -f results/*
|
||||
docker-compose build
|
||||
docker-compose up --abort-on-container-exit
|
||||
docker-compose down
|
||||
docker-compose rm --all
|
||||
Reference in new issue
Block a user