15 Commits

Author SHA1 Message Date
f82cf425fd Merge branch 'development' into 'master'
Merge preparation for Cathrine



See merge request !13
2016-05-28 20:47:26 +02:00
7696fb870c Merge branch 'docker_compose' into 'development'
Docker compose



See merge request !12
2016-05-28 17:13:22 +02:00
63ff5845e2 added demo config 2016-05-26 13:54:51 +02:00
10494a03a9 first version for docker-compose 2016-05-26 12:08:24 +02:00
2176579baa Merge branch 'implement_delete_account' into 'development'
Implement delete account



See merge request !11
2016-05-25 09:41:15 +02:00
da7c40c78b Merge branch 'index_page' into implement_delete_account 2016-05-25 09:40:20 +02:00
00579eb542 Merge branch 'index_page' into implement_delete_account 2016-05-24 23:56:12 +02:00
ef55b4e479 First step 2016-05-24 23:55:58 +02:00
a6c5abfd88 Merge branch 'index_page' into 'development'
Index page



See merge request !10
2016-05-24 07:02:33 +02:00
293ff50809 Some more UI changes 2016-05-24 06:49:39 +02:00
873a28aa28 ui redesign 2016-05-22 11:47:46 +02:00
1c4d73da43 Adds start page for unauthed visitors 2016-05-18 08:19:11 +02:00
7eef2b6cee Adds start page for unauthed visitors 2016-05-18 08:17:53 +02:00
227dc79e6b Merge branch 'issue_1' into 'development'
Handle vehicle name uniqueness

Vehicle names now must be only unique per owner, not globally.
Also errors are displayed to the forms on creation and edit of
vehicles.

See merge request !9
2016-05-16 18:49:39 +02:00
19b1e3b7ae Handle vehicle name uniqueness
Vehicle names now must be only unique per owner, not globally.
Also errors are displayed to the forms on creation and edit of
vehicles.
2016-05-16 18:46:24 +02:00
24 changed files with 527 additions and 222 deletions

View File

@@ -11,6 +11,9 @@ a folder that will serve as configuration directory and fill in the
information. The directory will be used as volume during container information. The directory will be used as volume during container
operation. operation.
## start database
`docker run --name pitstops_db -e MYSQL_ROOT_PASSWORD=$SOMESECUREPASSWORD$ -e MYSQL_DATABASE=pitstops -d mysql:latest`
## run in development ## run in development
Include the development version of the code as volume, so the app gets Include the development version of the code as volume, so the app gets
@@ -22,3 +25,4 @@ debugging during development.
## run in production ## run in production
`docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch` `docker run --name pitstops -d -v /data/pitstops/:/data -v /configs/pitstops/:/app/config -p 80:5000 rollerverbrauch`

View File

@@ -9,6 +9,7 @@ from flask.ext.security import Security, SQLAlchemyUserDatastore, \
from flask.ext.security import user_registered from flask.ext.security import user_registered
from flask_security.core import current_user from flask_security.core import current_user
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask.ext.security.forms import LoginForm
app = Flask(__name__) app = Flask(__name__)
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512' app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
@@ -21,17 +22,14 @@ app.config.from_object(__name__)
db = SQLAlchemy(app) db = SQLAlchemy(app)
mail = Mail(app) mail = Mail(app)
from rollerverbrauch.tools import \ import rollerverbrauch.tools as tools
VehicleStats, \
db_log_add, \
db_log_delete, \
db_log_update
from rollerverbrauch.forms import \ from rollerverbrauch.forms import \
CreatePitstopForm, \ CreatePitstopForm, \
EditVehicleForm, \ EditVehicleForm, \
DeleteVehicleForm, \ DeleteVehicleForm, \
SelectVehicleForm SelectVehicleForm, \
DeleteAccountForm
from rollerverbrauch.entities import \ from rollerverbrauch.entities import \
User, \ User, \
@@ -58,8 +56,8 @@ def user_registered_sighandler(app, user, confirm_token):
db.session.add(new_vehicle) db.session.add(new_vehicle)
user.vehicles.append(new_vehicle) user.vehicles.append(new_vehicle)
db.session.commit() db.session.commit()
db_log_add(user) tools.db_log_add(user)
db_log_add(new_vehicle) tools.db_log_add(new_vehicle)
@app.before_first_request @app.before_first_request
@@ -76,9 +74,28 @@ def before_request():
@app.route('/') @app.route('/')
@login_required
def index(): def index():
return redirect(url_for('get_pit_stops')) if current_user.is_authenticated:
return redirect(url_for('get_pit_stops'))
else:
user_count = len(User.query.all())
vehicles = Vehicle.query.all()
litres = 0
kilometers = 0
for vehicle in vehicles:
stats = tools.VehicleStats(vehicle)
litres += stats.overall_litres
kilometers += stats.overall_distance
vehicle_count = len(vehicles)
pitstop_count = len(Pitstop.query.all())
data = {
'users':user_count,
'vehicles': vehicle_count,
'pitstops': pitstop_count,
'litres': litres,
'kilometers': kilometers
}
return render_template('index.html', login_user_form=LoginForm(), data=data)
@app.route('/account/edit_vehicle/<int:vid>', methods=['GET', 'POST']) @app.route('/account/edit_vehicle/<int:vid>', methods=['GET', 'POST'])
@@ -88,9 +105,11 @@ def edit_vehicle(vid):
form = EditVehicleForm() form = EditVehicleForm()
if form.validate_on_submit(): if form.validate_on_submit():
if not tools.check_vehicle_name_is_unique(current_user, form.name):
return render_template('editVehicleForm.html', form=form)
vehicle.name = form.name.data vehicle.name = form.name.data
db.session.commit() db.session.commit()
db_log_update(vehicle) tools.db_log_update(vehicle)
return redirect(url_for('get_account_page')) return redirect(url_for('get_account_page'))
form.name.default = vehicle.name form.name.default = vehicle.name
@@ -112,7 +131,7 @@ def delete_vehicle(vid):
if form.validate_on_submit(): if form.validate_on_submit():
db.session.delete(vehicle) db.session.delete(vehicle)
db.session.commit() db.session.commit()
db_log_delete(vehicle) tools.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)
@@ -124,11 +143,14 @@ def create_vehicle():
form = EditVehicleForm() form = EditVehicleForm()
if form.validate_on_submit(): if form.validate_on_submit():
new_vehicle = Vehicle(form.name.data) vehicle_name = form.name.data
if not tools.check_vehicle_name_is_unique(current_user, form.name):
return render_template('createVehicleForm.html', form=form)
new_vehicle = Vehicle(vehicle_name)
db.session.add(new_vehicle) db.session.add(new_vehicle)
current_user.vehicles.append(new_vehicle) current_user.vehicles.append(new_vehicle)
db.session.commit() db.session.commit()
db_log_add(new_vehicle) tools.db_log_add(new_vehicle)
return redirect(url_for('get_account_page')) return redirect(url_for('get_account_page'))
return render_template('createVehicleForm.html', form=form) return render_template('createVehicleForm.html', form=form)
@@ -170,7 +192,7 @@ def create_pit_stop_form(vid):
db.session.add(new_stop) db.session.add(new_stop)
vehicle.pitstops.append(new_stop) vehicle.pitstops.append(new_stop)
db.session.commit() db.session.commit()
db_log_add(new_stop) tools.db_log_add(new_stop)
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 = last_pitstop.odometer form.odometer.default = last_pitstop.odometer
@@ -214,5 +236,18 @@ def get_account_page():
def get_statistics(): def get_statistics():
stats = [] stats = []
for vehicle in current_user.vehicles: for vehicle in current_user.vehicles:
stats.append(VehicleStats(vehicle)) stats.append(tools.VehicleStats(vehicle))
return render_template('statistics.html', data=stats) return render_template('statistics.html', data=stats)
@app.route('/account/delete', methods=['GET', 'POST'])
@login_required
def delete_account():
form = DeleteAccountForm()
if form.validate_on_submit():
user_datastore.delete_user(current_user)
db.session.commit()
return redirect(url_for('index'))
return render_template('deleteAccountForm.html', form=form)

View File

@@ -40,10 +40,11 @@ class User(db.Model, UserMixin):
class Vehicle(db.Model): class Vehicle(db.Model):
id = db.Column(db.Integer, primary_key=True) 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), unique=True) name = db.Column(db.String(255))
pitstops = db.relationship( pitstops = db.relationship(
'Pitstop' 'Pitstop'
) )
__table_args__ = (db.UniqueConstraint('owner_id', 'name', name='_owner_name_uniq'),)
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name

View File

@@ -44,3 +44,7 @@ class EditVehicleForm(Form):
class DeleteVehicleForm(Form): class DeleteVehicleForm(Form):
submit = SubmitField(label='Do it!') submit = SubmitField(label='Do it!')
class DeleteAccountForm(Form):
submit = SubmitField(label='Really delete my account!')

View File

@@ -1,8 +1,10 @@
body { body {
padding-top: 50px; padding-top: 50px;
} }
.starter-template { .starter-template {
padding: 40px 15px; padding-top: 30px;
padding-bottom: 60px;
text-align: center; text-align: center;
} }
@@ -22,9 +24,55 @@ td {
color: #a94442; color: #a94442;
} }
.pitstop {
}
.nav-pills > li > a { .nav-pills > li > a {
border: 1px solid; border: 1px solid;
}
h1 {
margin-top: 0px;
}
h2 {
margin-top: 0px;
}
h3 {
margin-top: 0px;
text-align: center;
}
h3:before{
content:"― ";
}
h3:after{
content:" ―";
}
.tab-content > .active {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 1px;
padding-top: 15px;
}
.panel-body > p, .panel-body > ul {
text-align: left;
}
// for small devices
@media only screen
and (min-device-width : 320px)
and (max-device-width : 568px) {
h3:before{
content:"";
}
h3:after{
content:"";
}
#charts_tabs {
display:none;
}
#charts_tabs-content {
display:none;
}
} }

View File

@@ -1,11 +1,11 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>Account management for {{current_user.email}}</h3> <h3>Account management for {{current_user.email}}</h3>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Password</div> <div class="panel-heading">Password</div>
<div class="panel-body"> <div class="panel-body">
<a href='{{ url_for('security.change_password') }}'> <a href='{{ url_for('security.change_password') }}' class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Change
</a> </a>
</div> </div>
@@ -13,44 +13,54 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Vehicles</div> <div class="panel-heading">Vehicles</div>
<div class="panel-body"> <div class="panel-body">
<a href="{{ url_for('create_vehicle') }}"> <a href="{{ url_for('create_vehicle') }}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> create
</a> </a>
</div> </div>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<tr> <tbody>
<th>
Vehicle
</th>
<th>
Info
</th>
<th>
Actions
</th>
</tr>
{% for vehicle in current_user.vehicles %}
<tr> <tr>
<td style="text-align:center"> <th>
{{ vehicle.name }} Vehicle
</td> </th>
<td style="text-align:center"> <th>
{{ vehicle.pitstops | length }} pitstops Info
</td> </th>
<td style="text-align:center"> <th>
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}"> Actions
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit </th>
</a>
{% if current_user.vehicles | length > 1 %}
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
{% else %}
&nbsp;
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% for vehicle in current_user.vehicles %}
<tr>
<td>
{{ vehicle.name }}
</td>
<td>
{{ vehicle.pitstops | length }} pitstops
</td>
<td>
<a href="{{ url_for('edit_vehicle', vid=vehicle.id) }}" class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> edit
</a>
{% if current_user.vehicles | length > 1 %}
<a href="{{ url_for('delete_vehicle', vid=vehicle.id) }}" class="btn btn-primary btn-warning " role="button">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span> delete
</a>
{% else %}
&nbsp;
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table> </table>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">Account</div>
<div class="panel-body">
<a href='{{ url_for('delete_account') }}' class="btn btn-primary " role="button">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete
</a>
</div>
</div>
{% endblock %} {% endblock %}

View File

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

View File

@@ -1,12 +1,18 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>Create vehicle</h3> <div class="col-md-2" ></div>
<form class='form-horizontal' method="POST"> <div class="col-md-8">
{{ form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(form.name) }} <div class="panel-body">
{{ render_field_with_errors(form.submit) }} <h3>Create vehicle</h3>
</form> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,18 @@
{% 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 account for '{{current_user.email}}'?</h3>
This cannot be undone!
<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 %}

View File

@@ -1,11 +1,18 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>Delete vehicle '{{vehicle.name}}'</h3> <div class="col-md-2" ></div>
<form class='form-horizontal' method="POST"> <div class="col-md-8">
{{ form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(form.submit) }} <div class="panel-body">
</form> <h3>Delete vehicle '{{vehicle.name}}'?</h3>
<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 %} {% endblock %}

View File

@@ -1,12 +1,18 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>Edit vehicle</h3> <div class="col-md-2" ></div>
<form class='form-horizontal' method="POST"> <div class="col-md-8">
{{ form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(form.name) }} <div class="panel-body">
{{ render_field_with_errors(form.submit) }} <h3>Edit vehicle</h3>
</form> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "layout.html" %}
{% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %}
<div class="row">
<div class="col-md-4">
{{ render_login_form() }}
</div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-body" >
<h1>Join the pitstop community!</h1>
<p>There are already {{ data.users}} members with {{ data.vehicles }} vehicles who have logged {{ data.pitstops }} pitstops fuelling {{ data.litres }}l for {{ data.kilometers }}km.</p>
<p>With pitstop community you can:</p>
<ul>
<li>manage multiple vehicles</li>
<li>track each pitstop</li>
<li>get statistics about the fuel consumption</li>
</ul>
<p><a href='{{ url_for('security.register') }}'>Register your account now</a> or <a href='{{ url_for('security.login') }}'>log into your account</a>.</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -16,14 +16,22 @@
{% macro render_field_with_errors(field) %} {% macro render_field_with_errors(field) %}
<div class="form-group"> <div class="form-group">
{% if field.type == 'SubmitField' %} {% if field.type == 'SubmitField' %}
<div class="col-sm-12" style="align:center"> <div class="col-md-4" ></div>
<div class="col-sm-4" style="align:center">
<input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}"> <input id="{{ field.id }}" name="{{ field.id }}" class="btn btn-default" type="submit" value="{{ field.label.text }}">
</div> </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>
{% else %} {% else %}
<label class="col-sm-6 control-label"> <label class="col-sm-6 control-label">
{{ field.label }} {{ field.label }}
</label> </label>
<div class="col-sm-2"> <div class="col-sm-6">
{% if field.type == 'SelectField' %} {% if field.type == 'SelectField' %}
<select id="{{ field.id }}" name="{{ field.id }}" class="form-control"> <select id="{{ field.id }}" name="{{ field.id }}" class="form-control">
{% for choice in field.choices %} {% for choice in field.choices %}
@@ -54,7 +62,25 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
</div>
{% endmacro %}
{% macro render_login_form() %}
<div class="panel panel-default">
<div class="panel-body">
<h3>Login</h3>
<form class='form-horizontal' action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ login_user_form.hidden_tag() }}
{{ render_field_with_errors(login_user_form.email) }}
{{ render_field_with_errors(login_user_form.password) }}
{{ render_field_with_errors(login_user_form.remember) }}
{{ render_field(login_user_form.next) }}
{{ render_field_with_errors(login_user_form.submit) }}
{% if security.recoverable %}
<a href="{{ url_for_security('forgot_password') }}">Forgot password</a>
{% endif %}
</form>
</div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -134,7 +160,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="{{ url_for('get_pit_stops') }}">refuel journal</a> <a class="navbar-brand" href="{{ url_for('index') }}">refuel journal</a>
</div> </div>
<div id="navbar" class="collapse navbar-collapse"> <div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@@ -150,6 +176,14 @@
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
{#
<nav class="navbar navbar-inverse navbar-fixed-bottom">
<div class="container">
<div class="navbar-footer">
<a class="navbar-brand" href="">Imprint</a>
</div>
</div>
</nav>
#}
</body> </body>
</html> </html>

View File

@@ -1,20 +1,25 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>New Pitstop for '{{ vehicle.name }}'</h3> <div class="col-md-2" ></div>
<form class='form-horizontal' method="POST"> <div class="col-md-8">
{{ form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(form.date) }} <div class="panel-body">
<span id="{{form.date.id}}_help" class="help-block"> <h3>New Pitstop for '{{ vehicle.name }}'</h3>
{{messages['date']}} <form class='form-horizontal' method="POST">
</span> {{ form.hidden_tag() }}
{{ render_field_with_errors(form.odometer) }} {{ render_field_with_errors(form.date) }}
<span id="{{form.odometer.id}}_help" class="help-block"> <span id="{{form.date.id}}_help" class="help-block">
{{messages['odometer']}} {{messages['date']}}
</span> </span>
{{ render_field_with_errors(form.litres) }} {{ render_field_with_errors(form.odometer) }}
{{ render_field_with_errors(form.submit) }} <span id="{{form.odometer.id}}_help" class="help-block">
</form> {{messages['odometer']}}
</span>
{{ render_field_with_errors(form.litres) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<div id="content"> <div id="content">
<ul id="tabs" class="nav nav-tabs" data-tabs="tabs"> <ul id="tabs" class="nav nav-tabs" data-tabs="tabs">
@@ -14,60 +14,66 @@
{% for vehicle in current_user.vehicles %} {% for vehicle in current_user.vehicles %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}"> <div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3> <h3>{{vehicle.name}}</h3>
<div class="table-responsive"> {% if vehicle.pitstops %}
<table class="table table-striped table-bordered table-condensed"> <div class="table-responsive">
<tr> <table class="table table-striped table-bordered table-condensed">
<th> <tr>
Date<br/> <th>
Days Date<br/>
</th> Days
<th> </th>
Odometer<br/> <th>
Distance Odometer<br/>
</th> Distance
<th> </th>
Litres<br/> <th>
Average Litres<br/>
</th> Average
</tr> </th>
{% for pitstop in vehicle.pitstops|reverse %} </tr>
{% if not loop.last %} {% for pitstop in vehicle.pitstops|reverse %}
{% set days = (pitstop.date - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].date).days %} {% if not loop.last %}
{% set distance = pitstop.odometer - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].odometer %} {% set days = (pitstop.date - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].date).days %}
{% set average = (pitstop.litres / distance) * 100 %} {% set distance = pitstop.odometer - vehicle.pitstops[vehicle.pitstops|length - loop.index - 1].odometer %}
<tr class='pitstop'> {% set average = (pitstop.litres / distance) * 100 %}
<td> <tr class='pitstop'>
{{pitstop.date}}<br/> <td>
{{ days }} days {{pitstop.date}}<br/>
</td> {{ days }} days
<td> </td>
{{pitstop.odometer}} km<br/> <td>
{{distance}} km {{pitstop.odometer}} km<br/>
</td> {{distance}} km
<td> </td>
{{pitstop.litres}} l<br/> <td>
{{average | round(2)}} l/100km {{pitstop.litres}} l<br/>
</td> {{average | round(2)}} l/100km
</tr> </td>
{% else %} </tr>
<tr class='pitstop'> {% else %}
<td> <tr class='pitstop'>
{{pitstop.date}}<br/> <td>
-- days {{pitstop.date}}<br/>
</td> -- days
<td> </td>
{{pitstop.odometer}} km<br/> <td>
-- km {{pitstop.odometer}} km<br/>
</td> -- km
<td> </td>
{{pitstop.litres}} l<br/> <td>
-- l/100km {{pitstop.litres}} l<br/>
</td> -- l/100km
</tr> </td>
{% endif %} </tr>
{% endfor %} {% endif %}
</table> {% endfor %}
</div> </table>
</div>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -2,12 +2,19 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %} {% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %} {% block body %}
<h1>Change password</h1> <div class="col-md-2" ></div>
<form class='form-horizontal' action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form"> <div class="col-md-8">
{{ change_password_form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(change_password_form.password) }} <div class="panel-body">
{{ render_field_with_errors(change_password_form.new_password) }} <h3>Change password</h3>
{{ render_field_with_errors(change_password_form.new_password_confirm) }} <form class='form-horizontal' action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form">
{{ render_field_with_errors(change_password_form.submit) }} {{ change_password_form.hidden_tag() }}
</form> {{ render_field_with_errors(change_password_form.password) }}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
{{ render_field_with_errors(change_password_form.submit) }}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -2,10 +2,18 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %} {% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %} {% block body %}
<h1>Send password reset instructions</h1> <div class="col-md-2" ></div>
<form class='form-horizontal' action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form"> <div class="col-md-8">
{{ forgot_password_form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(forgot_password_form.email) }} <div class="panel-body">
{{ render_field_with_errors(forgot_password_form.submit) }} <h3>Reset password</h3>
</form> <form class='form-horizontal' action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form">
{{ forgot_password_form.hidden_tag() }}
{{ render_field_with_errors(forgot_password_form.email) }}
{{ render_field_with_errors(forgot_password_form.submit) }}
</form>
</div>
</div>
<div class="col-md-2" ></div>
</div>
{% endblock %} {% endblock %}

View File

@@ -2,16 +2,12 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %} {% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %} {% block body %}
<h1>Login</h1> <div class="row">
<form class='form-horizontal' action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> <div class="col-md-2" ></div>
{{ login_user_form.hidden_tag() }} <div class="col-md-8">
{{ render_field_with_errors(login_user_form.email) }} {{ render_login_form() }}
{{ render_field_with_errors(login_user_form.password) }} </div>
{{ render_field_with_errors(login_user_form.remember) }} <div class="col-md-2" ></div>
{{ render_field(login_user_form.next) }} </div>
{{ render_field_with_errors(login_user_form.submit) }}
{% if security.recoverable %}
<a href="{{ url_for_security('forgot_password') }}">Forgot password</a>
{% endif %}
</form>
{% endblock %} {% endblock %}

View File

@@ -2,14 +2,22 @@
{% from "security/_macros.html" import render_field_with_errors, render_field %} {% from "security/_macros.html" import render_field_with_errors, render_field %}
{% block body %} {% block body %}
<h1>Register User</h1> <div class="col-md-2" ></div>
<form class='form-horizontal' action="{{ url_for_security('register') }}" method="POST" name="register_user_form"> <div class="col-md-8">
{{ register_user_form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(register_user_form.email) }} <div class="panel-body">
{{ render_field_with_errors(register_user_form.password) }} <h3>Register</h3>
{% if register_user_form.password_confirm %} <form class='form-horizontal' action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ render_field_with_errors(register_user_form.password_confirm) }} {{ register_user_form.hidden_tag() }}
{% endif %} {{ render_field_with_errors(register_user_form.email) }}
{{ render_field_with_errors(register_user_form.submit) }} {{ render_field_with_errors(register_user_form.password) }}
</form> {% if register_user_form.password_confirm %}
{{ render_field_with_errors(register_user_form.password_confirm) }}
{% endif %}
{{ render_field_with_errors(register_user_form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %} {% endblock %}

View File

@@ -1,11 +1,19 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h3>Select Vehicle</h3> <div class="col-md-2" ></div>
<form class='form-horizontal' method="POST"> <div class="col-md-8">
{{ form.hidden_tag() }} <div class="panel panel-default">
{{ render_field_with_errors(form.vehicle) }} <div class="panel-body">
{{ render_field_with_errors(form.submit) }} <h3>Select Vehicle</h3>
</form> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }}
{{ render_field_with_errors(form.vehicle) }}
{{ render_field_with_errors(form.submit) }}
</form>
</div>
</div>
</div>
<div class="col-md-2" ></div>
{% endblock %} {% endblock %}

View File

@@ -11,7 +11,7 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<div id="vehicle_contetn" class="tab-content"> <div id="vehicle_content" class="tab-content ">
{% for vehicle in data %} {% for vehicle in data %}
<div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}"> <div class="tab-pane {% if loop.first %}active{% endif %}" id="v{{vehicle.id}}">
<h3>{{vehicle.name}}</h3> <h3>{{vehicle.name}}</h3>
@@ -45,6 +45,11 @@
</div> </div>
<ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs"> <ul id="charts_tabs" class="nav nav-tabs" data-tabs="tabs">
<li class="active"> <li class="active">
<a href="#v{{vehicle.id}}_c3" id="i{{vehicle.id}}_c3" data-toggle="tab">
Consumption
</a>
</li>
<li>
<a href="#v{{vehicle.id}}_c1" id="i{{vehicle.id}}_c1" data-toggle="tab"> <a href="#v{{vehicle.id}}_c1" id="i{{vehicle.id}}_c1" data-toggle="tab">
Fuelled litres Fuelled litres
</a> </a>
@@ -54,21 +59,30 @@
Odometer Odometer
</a> </a>
</li> </li>
<li>
<a href="#v{{vehicle.id}}_c3" id="i{{vehicle.id}}_c3" data-toggle="tab">
Consumption
</a>
</li>
</ul> </ul>
<div id="my-tab-content" class="tab-content"> <div id="charts_tabs-content" class="tab-content">
<div class="tab-pane active" id="v{{vehicle.id}}_c1"> <div class="tab-pane active" id="v{{vehicle.id}}_c3">
{% if vehicle.pitstop_count > 1 %}
<div id="averageUsageDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript">
{{ chartScript('averageUsageDiv'+vehicle.id|str, vehicle.average_litres, 'l/100 km') }}
</script>
{% else %}
<div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %}
</div>
<div class="tab-pane " id="v{{vehicle.id}}_c1">
{% if vehicle.pitstop_count > 0 %} {% if vehicle.pitstop_count > 0 %}
<div id="fuelledChartDiv{{vehicle.id}}" style="width:100%; height:500px;"></div> <div id="fuelledChartDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript"> <script type="text/javascript">
{{ chartScript('fuelledChartDiv'+vehicle.id|str, vehicle.litres, 'l') }} {{ chartScript('fuelledChartDiv'+vehicle.id|str, vehicle.litres, 'l') }}
</script> </script>
{% else %} {% else %}
not enough data. <div class="alert alert-warning" role="alert">
not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane " id="v{{vehicle.id}}_c2"> <div class="tab-pane " id="v{{vehicle.id}}_c2">
@@ -78,17 +92,9 @@
{{ chartScript('odometerChartDiv'+vehicle.id|str, vehicle.odometers, 'km') }} {{ chartScript('odometerChartDiv'+vehicle.id|str, vehicle.odometers, 'km') }}
</script> </script>
{% else %} {% else %}
not enough data. <div class="alert alert-warning" role="alert">
{% endif %} not enough data: <a href="{{ url_for('create_pit_stop_form', vid=vehicle.id) }}">log a pitstop</a>?
</div> </div>
<div class="tab-pane " id="v{{vehicle.id}}_c3">
{% if vehicle.pitstop_count > 1 %}
<div id="averageUsageDiv{{vehicle.id}}" style="width:100%; height:500px;"></div>
<script type="text/javascript">
{{ chartScript('averageUsageDiv'+vehicle.id|str, vehicle.average_litres, 'l/100 km') }}
</script>
{% else %}
not enough data.
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -47,3 +47,21 @@ def db_log_delete(entity):
def db_log_update(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):
"""
Checks if the vehicle name given in the name_field is unique for the vehicles of the current user. An error is added
to the field it the name is not unique.
:param current_user: the user currently logged in
:param name_field: the form field to enter the name to
:return: True if the name is unique, False otherwise.
"""
vehicle_name = name_field.data
for vehicle in current_user.vehicles:
if vehicle.name == vehicle_name:
name_field.default = vehicle_name
name_field.errors.append('Vehicle "%s" already exists.' % vehicle_name)
return False
return True

16
compose_config/config.py Normal file
View File

@@ -0,0 +1,16 @@
import os
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:%s@database/pitstops' % (os.environ['DATABASE_ENV_MYSQL_ROOT_PASSWORD'])
#SQLALCHEMY_DATABASE_URI = 'sqlite:////data/rollerverbrauch.db'
MAIL_SERVER = ''
MAIL_PORT= 25
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_USERNAME = ''
MAIL_PASSWORD = ''
SECURITY_EMAIL_SENDER = ''
SECURITY_PASSWORD_SALT = 'SecretSalt'
SECRET_KEY = 'SecretKey'
SECURITY_SEND_REGISTER_EMAIL = False

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
version: '2'
services:
rollerverbrauch:
build: .
depends_on:
- database
volumes:
- ./compose_config/:/config
environment:
- config=/config/config.py
- DATABASE_ENV_MYSQL_ROOT_PASSWORD=foobar123
ports:
- 5000
database:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=foobar123
- MYSQL_DATABASE=pitstops
ports:
- 3306