Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f01b4e368 | ||
|
|
be60601f26 | ||
|
|
24c0b4c215 | ||
|
|
208cb497aa | ||
|
|
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 |
No files matched your search
+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`
|
||||
+11
-3
@@ -1,12 +1,12 @@
|
||||
from flask import Flask
|
||||
from flask import Flask, make_response
|
||||
from flask import g
|
||||
from flask_mail import Mail
|
||||
from flask_security import Security, SQLAlchemyUserDatastore, user_registered
|
||||
from flask_security.forms import LoginForm
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
import os
|
||||
from config import config
|
||||
from flask.ext.security.forms import LoginForm
|
||||
from flask_security.forms import LoginForm
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
from .forms import *
|
||||
|
||||
@@ -14,6 +14,13 @@ 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)
|
||||
|
||||
@@ -50,6 +57,7 @@ def user_registered_sighandler(application, 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()
|
||||
|
||||
+76
-45
@@ -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,29 +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'
|
||||
)
|
||||
services = db.relationship(
|
||||
'Service'
|
||||
)
|
||||
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):
|
||||
@@ -93,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
|
||||
@@ -117,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):
|
||||
@@ -128,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
|
||||
@@ -149,7 +178,7 @@ 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'))
|
||||
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))
|
||||
|
||||
@@ -161,5 +190,7 @@ class Service(db.Model):
|
||||
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)
|
||||
return (
|
||||
'<Service odometer="%r" date="%r" vehicle_id="%r" costs="%r" description="%r">'
|
||||
% (self.odometer, self.date, self.vehicle_id, self.costs, self.description)
|
||||
)
|
||||
@@ -4,3 +4,4 @@ from .checks import *
|
||||
from .consumable import *
|
||||
from .service import *
|
||||
from .vehicle import *
|
||||
from .regular_cost import *
|
||||
+133
-12
@@ -2,18 +2,130 @@ 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.
|
||||
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)
|
||||
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())
|
||||
raise ValidationError(
|
||||
"The new date must not be after %s" % date.today()
|
||||
)
|
||||
|
||||
|
||||
def odometer_check(form, field):
|
||||
@@ -23,20 +135,29 @@ def odometer_check(form, field):
|
||||
: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 (
|
||||
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)
|
||||
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')
|
||||
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 €.')
|
||||
raise ValidationError("Costs must be above 0.01 €.")
|
||||
|
||||
|
||||
def edit_costs_check(form, field):
|
||||
@@ -46,8 +167,8 @@ def edit_costs_check(form, field):
|
||||
:param field:
|
||||
:return:
|
||||
"""
|
||||
costs_check_required = (form.costs.default is not None and form.costs.default > 0)
|
||||
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 €.')
|
||||
|
||||
|
||||
raise ValidationError("Costs must be above 0.01 €.")
|
||||
+31
-20
@@ -9,16 +9,16 @@ class DeletePitStopForm(FlaskForm):
|
||||
|
||||
|
||||
class EditPitstopForm(FlaskForm):
|
||||
date = DateField('Date of Pitstop', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
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=[edit_costs_check])
|
||||
costs = DecimalField('Costs (€, overall)', places=2, validators=[costs_check])
|
||||
submit = SubmitField(label='Update it!')
|
||||
last_pitstop = None
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
@@ -33,18 +33,25 @@ class EditPitstopForm(FlaskForm):
|
||||
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', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
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!')
|
||||
last_pitstop = None
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def set_consumable(self, consumable):
|
||||
self.litres.label = '%s (%s)' % (consumable.name, consumable.unit)
|
||||
@@ -54,27 +61,31 @@ class CreatePitstopForm(FlaskForm):
|
||||
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 = self.last_pitstop.odometer
|
||||
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 = self.last_pitstop.amount
|
||||
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 = self.last_pitstop.costs
|
||||
self.costs.default = 0
|
||||
|
||||
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)),
|
||||
'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
|
||||
+24
-17
@@ -6,15 +6,15 @@ from .checks import *
|
||||
|
||||
|
||||
class CreateServiceForm(FlaskForm):
|
||||
date = DateField('Date of Service', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
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!')
|
||||
last_pitstop = None
|
||||
pitstops = []
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
def set_pitstops(self, pitstops):
|
||||
self.pitstops = pitstops
|
||||
|
||||
def preinit_with_data(self):
|
||||
if self.date.data:
|
||||
@@ -24,45 +24,52 @@ class CreateServiceForm(FlaskForm):
|
||||
|
||||
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 = self.last_pitstop.odometer
|
||||
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', validators=[date_check])
|
||||
odometer = IntegerField('Odometer (km)', validators=[odometer_check])
|
||||
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!')
|
||||
last_pitstop = None
|
||||
same_odometer_allowed = True
|
||||
pitstops = []
|
||||
|
||||
def set_pitstop(self, last_pitstop):
|
||||
self.last_pitstop = last_pitstop
|
||||
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
|
||||
else:
|
||||
self.odometer.default = self.last_pitstop.odometer
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
+14
-9
@@ -1,20 +1,25 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, SelectField, SelectMultipleField
|
||||
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!')
|
||||
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=[])
|
||||
submit = SubmitField(label='Do it!')
|
||||
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!')
|
||||
|
||||
|
||||
submit = SubmitField(label="Do it!")
|
||||
@@ -3,3 +3,4 @@ from .admin import *
|
||||
from .misc import *
|
||||
from .pitstop import *
|
||||
from .service import *
|
||||
from .regular_cost import *
|
||||
+32
-23
@@ -1,7 +1,8 @@
|
||||
from flask import url_for, redirect, render_template
|
||||
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
|
||||
@@ -9,20 +10,23 @@ from ..tools import db_log_update, db_log_delete, db_log_add
|
||||
from .. import app, db, user_datastore
|
||||
|
||||
|
||||
@app.route('/account', methods=['GET'])
|
||||
@app.route("/account", methods=["GET"])
|
||||
@login_required
|
||||
def get_account_page():
|
||||
return render_template('account.html')
|
||||
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'])
|
||||
@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'))
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = EditVehicleForm()
|
||||
form.consumables.choices = [(g.id, g.name) for g in Consumable.query.all()]
|
||||
@@ -33,11 +37,15 @@ def edit_vehicle(vid):
|
||||
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:
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
vehicle.consumables.append(consumable)
|
||||
@@ -47,25 +55,26 @@ def edit_vehicle(vid):
|
||||
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'))
|
||||
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)
|
||||
return render_template("editVehicleForm.html", form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route('/account/vehicle/delete/<int:vid>', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for('get_account_page'))
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = DeleteVehicleForm()
|
||||
|
||||
@@ -73,12 +82,12 @@ def delete_vehicle(vid):
|
||||
db.session.delete(vehicle)
|
||||
db.session.commit()
|
||||
db_log_delete(vehicle)
|
||||
return redirect(url_for('get_account_page'))
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
return render_template('deleteVehicleForm.html', form=form, vehicle=vehicle)
|
||||
return render_template("deleteVehicleForm.html", form=form, vehicle=vehicle)
|
||||
|
||||
|
||||
@app.route('/account/vehicle/create', methods=['GET', 'POST'])
|
||||
@app.route("/account/vehicle/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_vehicle():
|
||||
form = EditVehicleForm()
|
||||
@@ -94,8 +103,8 @@ def create_vehicle():
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -111,13 +120,13 @@ def create_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)
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
return render_template('createVehicleForm.html', form=form)
|
||||
return render_template("createVehicleForm.html", form=form)
|
||||
|
||||
|
||||
@app.route('/account/delete', methods=['GET', 'POST'])
|
||||
@app.route("/account/delete", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_account():
|
||||
form = DeleteAccountForm()
|
||||
@@ -125,7 +134,7 @@ def delete_account():
|
||||
if form.validate_on_submit():
|
||||
user_datastore.delete_user(current_user)
|
||||
db.session.commit()
|
||||
return redirect(url_for('index'))
|
||||
return redirect(url_for("index"))
|
||||
|
||||
return render_template('deleteAccountForm.html', form=form)
|
||||
return render_template("deleteAccountForm.html", form=form)
|
||||
|
||||
+8
-8
@@ -6,17 +6,17 @@ from ..tools import VehicleStats
|
||||
from .. import app
|
||||
|
||||
|
||||
@app.route('/statistics', methods=['GET'])
|
||||
@app.route("/statistics", methods=["GET"])
|
||||
@login_required
|
||||
def get_statistics():
|
||||
stats = []
|
||||
for vehicle in current_user.vehicles:
|
||||
|
||||
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)
|
||||
return render_template("statistics.html", data=stats)
|
||||
|
||||
|
||||
@app.route('/manual', methods=['GET'])
|
||||
@login_required
|
||||
def get_manual():
|
||||
return render_template('manual.html')
|
||||
|
||||
+113
-66
@@ -3,73 +3,106 @@ 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, get_latest_pitstop_for_vehicle, \
|
||||
get_latest_pitstop_for_vehicle_and_consumable, compute_lower_limits_for_new_pitstop, pitstop_service_key, \
|
||||
get_event_line_for_vehicle
|
||||
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'])
|
||||
@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))
|
||||
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 current_user.vehicles]
|
||||
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 redirect(
|
||||
url_for("select_consumable_for_new_pitstop", vid=form.vehicle.data)
|
||||
)
|
||||
|
||||
return render_template('selectVehicle.html', form=form)
|
||||
return render_template("selectVehicle.html", form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/vehicle/<int:vid>/consumable/select', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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))
|
||||
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))
|
||||
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 redirect(
|
||||
url_for("create_pit_stop_form", vid=vid, cid=form.consumable.data)
|
||||
)
|
||||
|
||||
return render_template('selectConsumableForVehicle.html', vehicle=vehicle, form=form)
|
||||
return render_template(
|
||||
"selectConsumableForVehicle.html", vehicle=vehicle, form=form
|
||||
)
|
||||
|
||||
|
||||
@app.route('/pitstops/vehicle/<int:vid>/consumable/<int:cid>/create', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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))
|
||||
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_pitstop(Pitstop(data[-1].odometer, 0, data[-1].date, 0, cid))
|
||||
form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (data[-1].consumable.id != cid)
|
||||
form.set_pitstops(data)
|
||||
form.same_odometer_allowed = (type(data[-1]) != Pitstop) or (
|
||||
data[-1].consumable.id != cid
|
||||
)
|
||||
else:
|
||||
form.set_pitstop(Pitstop(date(1970, 1, 1), 0, vid, 0, ''))
|
||||
form.set_pitstop(Pitstop(0, 0, date(1970, 1, 1), 0, cid))
|
||||
form.set_pitstops([])
|
||||
form.same_odometer_allowed = True
|
||||
|
||||
# set the label of the litres field to make the user comfortable
|
||||
@@ -82,7 +115,9 @@ def create_pit_stop_form(vid, cid):
|
||||
# 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)
|
||||
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:
|
||||
@@ -90,54 +125,67 @@ def create_pit_stop_form(vid, cid):
|
||||
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.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())
|
||||
return render_template(
|
||||
"createPitStopForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route('/pitstops/delete/<int:pid>', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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'))
|
||||
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 redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template('deletePitstopForm.html', form=form, pitstop=pitstop )
|
||||
return render_template("deletePitstopForm.html", form=form, pitstop=pitstop)
|
||||
|
||||
|
||||
@app.route('/pitstops/edit/<int:pid>', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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)
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = EditPitstopForm()
|
||||
form.set_pitstop(last_pitstop)
|
||||
|
||||
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
|
||||
@@ -145,40 +193,39 @@ def edit_pit_stop_form(pid):
|
||||
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)))
|
||||
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.preinit_with_data()
|
||||
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)
|
||||
return render_template(
|
||||
"editPitStopForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route('/pitstops', methods=['GET'])
|
||||
@app.route("/pitstops", methods=["GET"])
|
||||
@login_required
|
||||
def get_pit_stops():
|
||||
user = {
|
||||
'vehicles': []
|
||||
}
|
||||
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
|
||||
"id": vehicle.id,
|
||||
"name": vehicle.name,
|
||||
"data": data,
|
||||
"regulars": vehicle.regulars,
|
||||
}
|
||||
user['vehicles'].append(v)
|
||||
user["vehicles"].append(v)
|
||||
user["vehicles"].sort(key=lambda v: (v["data"][-1].date, v["data"][-1].odometer or 0 ), reverse=true)
|
||||
|
||||
return render_template('pitstops.html', user=user)
|
||||
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,
|
||||
)
|
||||
+61
-44
@@ -3,83 +3,103 @@ 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
|
||||
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'])
|
||||
@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'))
|
||||
return redirect(url_for("get_account_page"))
|
||||
|
||||
form = CreateServiceForm()
|
||||
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
if len(data) > 0:
|
||||
form.set_pitstop(Service(data[-1].date, data[-1].odometer, vid, 0, ''))
|
||||
form.set_pitstops(data)
|
||||
form.same_odometer_allowed = type(data[-1]) != Service
|
||||
else:
|
||||
form.set_pitstop(Service(date(1970, 1, 1), 0, vid, 0, ''))
|
||||
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)
|
||||
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)))
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.process()
|
||||
return render_template('createServiceForm.html', form=form, vehicle=vehicle, messages=[])
|
||||
return render_template(
|
||||
"createServiceForm.html", form=form, vehicle=vehicle, messages=[]
|
||||
)
|
||||
|
||||
|
||||
@app.route('/service/delete/<int:sid>', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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'))
|
||||
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 redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
return render_template('deleteServiceForm.html', form=form, service=service )
|
||||
return render_template("deleteServiceForm.html", form=form, service=service)
|
||||
|
||||
|
||||
@app.route('/service/edit/<int:sid>', methods=['GET', 'POST'])
|
||||
@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'))
|
||||
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'))
|
||||
return redirect(url_for("get_pit_stops"))
|
||||
|
||||
form = EditServiceForm()
|
||||
data = get_event_line_for_vehicle(vehicle)
|
||||
data.reverse()
|
||||
if len(data) > 0:
|
||||
last_pitstop = Service(data[-1].date, data[-1].odometer, vehicle.id, 0, '')
|
||||
else:
|
||||
last_pitstop = Service(date(1970, 1, 1), 0, vehicle.id, 0, '')
|
||||
form.set_pitstop(last_pitstop)
|
||||
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
|
||||
@@ -87,34 +107,31 @@ def edit_service_form(sid):
|
||||
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)))
|
||||
return redirect(url_for("get_pit_stops", _anchor="v" + str(vehicle.id)))
|
||||
|
||||
form.odometer.default = edit_service.odometer
|
||||
form.description.default = edit_service.description
|
||||
form.date.default = edit_service.date
|
||||
form.costs.default = edit_service.costs
|
||||
form.preinit_with_data()
|
||||
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_service.costs is not None and edit_service.costs > 0:
|
||||
messages['costs'] = 'Costs must be higher than 0.01 €.'
|
||||
return render_template('editServiceForm.html', form=form, vehicle=vehicle, messages=messages)
|
||||
return render_template(
|
||||
"editServiceForm.html",
|
||||
form=form,
|
||||
vehicle=vehicle,
|
||||
messages=form.get_hint_messages(),
|
||||
)
|
||||
|
||||
|
||||
@app.route('/service/vehicle/select', methods=['GET', 'POST'])
|
||||
@app.route("/service/vehicle/select", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def select_vehicle_for_new_service():
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for('create_service_for_vehicle', vid=current_user.vehicles[0].id))
|
||||
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 current_user.vehicles]
|
||||
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)
|
||||
|
||||
return redirect(url_for("create_service_for_vehicle", vid=form.vehicle.data))
|
||||
|
||||
return render_template("selectVehicle.html", form=form)
|
||||
@@ -13,7 +13,7 @@ body {
|
||||
|
||||
.starter-template {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 60px;
|
||||
// padding-bottom: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -92,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}
|
||||
@@ -33,7 +33,10 @@
|
||||
{% 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 />
|
||||
|
||||
@@ -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,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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -10,6 +10,7 @@
|
||||
{{ 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 %}
|
||||
+40
-26
@@ -2,6 +2,7 @@
|
||||
{% if current_user.email %}
|
||||
<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') %}
|
||||
@@ -14,20 +15,17 @@
|
||||
{% 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 }}
|
||||
@@ -46,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' %}
|
||||
@@ -106,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">
|
||||
@@ -127,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">
|
||||
@@ -176,6 +177,19 @@
|
||||
{% 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 %}
|
||||
@@ -1,5 +1,92 @@
|
||||
{% 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">
|
||||
@@ -31,13 +118,13 @@
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -73,13 +160,13 @@
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
@@ -110,12 +197,22 @@
|
||||
{% 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>
|
||||
|
||||
+124
-45
@@ -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,6 +74,8 @@ 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))
|
||||
@@ -57,27 +88,49 @@ class VehicleStats:
|
||||
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 = 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:
|
||||
def __init__(self, date, value):
|
||||
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):
|
||||
@@ -104,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
|
||||
|
||||
|
||||
@@ -118,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:
|
||||
@@ -150,35 +207,10 @@ 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):
|
||||
return x.odometer, x.date
|
||||
# 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):
|
||||
@@ -188,4 +220,51 @@ def get_event_line_for_vehicle(vehicle):
|
||||
for service in vehicle.services:
|
||||
data.append(service)
|
||||
data.sort(key=pitstop_service_key)
|
||||
return data
|
||||
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__))
|
||||
|
||||
|
||||
@@ -18,6 +19,8 @@ class Config:
|
||||
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):
|
||||
@@ -27,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.get('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 = {
|
||||
@@ -44,4 +50,4 @@ config = {
|
||||
'testing': TestingConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
+12
-5
@@ -1,6 +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
|
||||
Reference in new issue
Block a user