Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9812bcb06 | |||
| 8b78dbe5b7 | |||
| 95a7e56a32 | |||
| 9eb4d5ada0 | |||
| f7cb273254 | |||
| 543d3e1658 | |||
| 9427ed50ad | |||
| 28990f27fa | |||
| d55fd3d9f6 | |||
| e9bb7986f4 | |||
| eaacd3f42e | |||
| 8a99e4a616 | |||
| 028b52d12f | |||
| 958a9bdd9f | |||
| b02938cafe | |||
| 47e81c7517 |
@@ -1,12 +1,13 @@
|
||||
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 import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
from .forms import *
|
||||
|
||||
@@ -14,6 +15,19 @@ from .forms import *
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default'])
|
||||
|
||||
limiter = Limiter(
|
||||
app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["200 per day", "50 per hour"]
|
||||
)
|
||||
|
||||
|
||||
@app.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
return make_response(
|
||||
jsonify(error="ratelimit exceeded %s" % e.description)
|
||||
, 429
|
||||
)
|
||||
db = SQLAlchemy(app)
|
||||
mail = Mail(app)
|
||||
|
||||
@@ -47,9 +61,21 @@ def user_registered_sighandler(application, user, confirm_token):
|
||||
tools.db_log_add(new_vehicle)
|
||||
|
||||
|
||||
def assure_consumable(name, ext_id, unit):
|
||||
if not Consumable.query.filter(Consumable.ext_id == ext_id).first():
|
||||
c = Consumable(name, ext_id, unit)
|
||||
db.session.add(c)
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def before_first_request():
|
||||
db.create_all()
|
||||
|
||||
# make sure all consumables from tankerkoenig exist: diesel, e5, e10
|
||||
assure_consumable('Diesel', 'diesel', 'L')
|
||||
assure_consumable('Super','e5', 'L')
|
||||
assure_consumable('Super E10','e10', 'L')
|
||||
|
||||
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()
|
||||
|
||||
@@ -10,6 +10,11 @@ vehicles_consumables = db.Table('vehicles_consumables',
|
||||
db.Column('consumable_id', db.Integer(), db.ForeignKey('consumable.id')))
|
||||
|
||||
|
||||
users_fillingstations = db.Table('users_fillingstations',
|
||||
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
|
||||
db.Column('fillingstation_id', db.Integer(), db.ForeignKey('filling_station.int_id')))
|
||||
|
||||
|
||||
class Role(db.Model, RoleMixin):
|
||||
"""
|
||||
Entity to handle different roles for users: Typically user and admin exist
|
||||
@@ -34,6 +39,9 @@ class User(db.Model, UserMixin):
|
||||
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'
|
||||
@@ -43,6 +51,10 @@ class User(db.Model, UserMixin):
|
||||
secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic')
|
||||
)
|
||||
favourite_filling_stations = db.relationship(
|
||||
'FillingStation',
|
||||
secondary=users_fillingstations
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User id="%r" email="%r" ' % (self.id, self.email)
|
||||
@@ -130,6 +142,7 @@ class Consumable(db.Model):
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), unique=True)
|
||||
ext_id = db.Column(db.String(255))
|
||||
unit = db.Column(db.String(255))
|
||||
|
||||
vehicles = db.relationship(
|
||||
@@ -137,8 +150,9 @@ class Consumable(db.Model):
|
||||
secondary=vehicles_consumables
|
||||
)
|
||||
|
||||
def __init__(self, name, unit):
|
||||
def __init__(self, name, ext_id, unit):
|
||||
self.name = name
|
||||
self.ext_id = ext_id
|
||||
self.unit = unit
|
||||
|
||||
def __repr__(self):
|
||||
@@ -163,3 +177,32 @@ class Service(db.Model):
|
||||
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)
|
||||
|
||||
|
||||
class FillingStation(db.Model):
|
||||
int_id = db.Column(db.Integer, primary_key=True)
|
||||
id = db.Column(db.String(40), unique=True, nullable=False)
|
||||
name = db.Column(db.Text(), nullable=False)
|
||||
street = db.Column(db.Text(), nullable=False)
|
||||
place = db.Column(db.Text(), nullable=False)
|
||||
houseNumber = db.Column(db.Text())
|
||||
postCode = db.Column(db.Integer(), nullable=False)
|
||||
brand = db.Column(db.Text(), nullable=False)
|
||||
lat = db.Column(db.Numeric(8, 5), nullable=False)
|
||||
lng = db.Column(db.Numeric(8, 5), nullable=False)
|
||||
last_update = db.Column(db.DateTime)
|
||||
diesel = db.Column(db.Numeric(10, 3), default=0)
|
||||
e5 = db.Column(db.Numeric(10, 3), default=0)
|
||||
e10 = db.Column(db.Numeric(10, 3), default=0)
|
||||
open = db.Column(db.Boolean())
|
||||
|
||||
def as_dict(self):
|
||||
res = {}
|
||||
for c in self.__table__.columns:
|
||||
val = getattr(self, c.name)
|
||||
import decimal
|
||||
if isinstance(val, decimal.Decimal):
|
||||
val = float(val)
|
||||
val = str(val)
|
||||
res[c.name] = val
|
||||
return res
|
||||
|
||||
@@ -10,12 +10,14 @@ class SelectConsumableForm(FlaskForm):
|
||||
|
||||
class CreateConsumableForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
ext_id = SelectField('Tankerkönig ID', coerce=int)
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
class EditConsumableForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Length(1, 255)])
|
||||
ext_id = SelectField('Tankerkönig ID', coerce=int)
|
||||
unit = StringField('Unit', validators=[Length(1, 255)])
|
||||
submit = SubmitField(label='Do it!')
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ from .admin import *
|
||||
from .misc import *
|
||||
from .pitstop import *
|
||||
from .service import *
|
||||
from .filling_stations import *
|
||||
@@ -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
|
||||
@@ -12,7 +13,12 @@ from .. import app, db, user_datastore
|
||||
@app.route('/account', methods=['GET'])
|
||||
@login_required
|
||||
def get_account_page():
|
||||
return render_template('account.html')
|
||||
stations = [x.as_dict() for x in current_user.favourite_filling_stations]
|
||||
for station in stations:
|
||||
station['state'] = 'favourite'
|
||||
return render_template('account.html',
|
||||
map_pos=(current_user.home_lat, current_user.home_long, current_user.home_zoom),
|
||||
fs=json.dumps(stations))
|
||||
|
||||
|
||||
@app.route('/account/vehicle/edit/<int:vid>', methods=['GET', 'POST'])
|
||||
@@ -37,7 +43,7 @@ def edit_vehicle(vid):
|
||||
vehicle.name = form.name.data
|
||||
# we cannot delete consumables where there are pitstops for => report error
|
||||
vehicle.consumables = []
|
||||
for consumable_id in form.consumables.data:
|
||||
for consumable_id in form.consumables.data:
|
||||
consumable = Consumable.query.get(consumable_id)
|
||||
if consumable is not None:
|
||||
vehicle.consumables.append(consumable)
|
||||
@@ -129,3 +135,21 @@ def delete_account():
|
||||
|
||||
return render_template('deleteAccountForm.html', form=form)
|
||||
|
||||
|
||||
@app.route('/account/home', methods=['GET'])
|
||||
@login_required
|
||||
def get_users_home():
|
||||
return jsonify(
|
||||
{'lat': float(current_user.home_lat), 'long': float(current_user.home_long), 'zoom': current_user.home_zoom})
|
||||
|
||||
|
||||
@app.route('/account/home', methods=['POST'])
|
||||
@login_required
|
||||
def set_users_home():
|
||||
current_user.home_lat = request.json['lat']
|
||||
current_user.home_long = request.json['long']
|
||||
current_user.home_zoom = request.json['zoom']
|
||||
db.session.commit()
|
||||
return jsonify({})
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ def get_admin_page():
|
||||
@login_required
|
||||
def create_consumable():
|
||||
form = CreateConsumableForm()
|
||||
choices = [(0, ''), (1, 'diesel'), (2, 'e5'), (3, 'e10')]
|
||||
form.ext_id.choices = choices
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
@@ -30,7 +32,7 @@ def create_consumable():
|
||||
form.unit.default = form.unit.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_consumable = Consumable(form.name.data, form.unit.data)
|
||||
new_consumable = Consumable(form.name.data, choices[form.ext_id.data][1], form.unit.data)
|
||||
db.session.add(new_consumable)
|
||||
try:
|
||||
db.session.commit()
|
||||
@@ -70,19 +72,28 @@ def edit_consumable(cid):
|
||||
return redirect(url_for('get_admin_page'))
|
||||
|
||||
form = EditConsumableForm()
|
||||
choices = [(0, ''), (1, 'diesel'), (2, 'e5'), (3, 'e10')]
|
||||
form.ext_id.choices = choices
|
||||
|
||||
form.name.default = consumable.name
|
||||
form.unit.default = consumable.unit
|
||||
form.ext_id.default = 3
|
||||
for c in choices:
|
||||
if c[1] == consumable.ext_id:
|
||||
form.ext_id.default = c[0]
|
||||
|
||||
# preinitialize the defaults with potentially existing values from a try before
|
||||
if form.name.data is not None:
|
||||
form.name.default = form.name.data
|
||||
if form.unit.data is not None:
|
||||
form.unit.default = form.unit.data
|
||||
if form.ext_id.data is not None:
|
||||
form.ext_id.default = form.ext_id.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
consumable.name = form.name.data
|
||||
consumable.unit = form.unit.data
|
||||
consumable.ext_id = choices[form.ext_id.data][1]
|
||||
try:
|
||||
db.session.commit()
|
||||
db_log_update(consumable)
|
||||
|
||||
62
app/routes/filling_stations.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from flask import request, jsonify
|
||||
from flask_security import login_required
|
||||
from flask_security.core import current_user
|
||||
import requests
|
||||
|
||||
from ..entities import FillingStation
|
||||
from .. import app, db, limiter
|
||||
|
||||
|
||||
@app.route('/filling_stations/favourites/toggle/<fsid>')
|
||||
def add_favourite_filling_stations(fsid):
|
||||
favourite_ids = {x.id: x for x in current_user.favourite_filling_stations}
|
||||
|
||||
if fsid in favourite_ids:
|
||||
current_user.favourite_filling_stations.remove(favourite_ids[fsid])
|
||||
state = 'normal'
|
||||
else:
|
||||
fs = FillingStation.query.filter(FillingStation.id == fsid).first()
|
||||
current_user.favourite_filling_stations.append(fs)
|
||||
state = 'favourite'
|
||||
db.session.commit()
|
||||
return jsonify({'state': state})
|
||||
|
||||
|
||||
@app.route('/filling_stations', methods=['GET'])
|
||||
@login_required
|
||||
@limiter.limit('1 per second')
|
||||
def query_filling_stations():
|
||||
api_key = app.config['TANKERKOENIG_API_KEY']
|
||||
|
||||
latitude = request.args.get('latitude')
|
||||
longitude = request.args.get('longitude')
|
||||
radius = request.args.get('radius', default=1.5)
|
||||
gas_type = request.args.get('type', default='all')
|
||||
sort = request.args.get('sort', default='dist')
|
||||
|
||||
url = 'https://creativecommons.tankerkoenig.de/json/list.php'
|
||||
params = {
|
||||
'lat': latitude, 'lng': longitude, 'rad': radius, 'apikey': api_key, 'type': gas_type, 'sort': sort
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
data = response.json()
|
||||
for station in data['stations']:
|
||||
fs = FillingStation.query.filter(FillingStation.id == station['id']).first()
|
||||
if not fs:
|
||||
fs = FillingStation()
|
||||
fs.id = station['id']
|
||||
fs.brand = station['brand']
|
||||
fs.lat = station['lat']
|
||||
fs.lng = station['lng']
|
||||
fs.name = station['name']
|
||||
fs.street = station['street']
|
||||
fs.place = station['place']
|
||||
fs.houseNumber = station['houseNumber']
|
||||
fs.postCode = station['postCode']
|
||||
db.session.add(fs)
|
||||
if fs in current_user.favourite_filling_stations:
|
||||
station['state'] = 'favourite'
|
||||
else:
|
||||
station['state'] = 'normal'
|
||||
db.session.commit()
|
||||
return jsonify(data)
|
||||
@@ -6,9 +6,8 @@ from datetime import date
|
||||
|
||||
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 ..tools import db_log_update, db_log_delete, db_log_add, pitstop_service_key, \
|
||||
get_event_line_for_vehicle, update_filling_station_prices
|
||||
from .. import app, db
|
||||
|
||||
|
||||
@@ -182,3 +181,63 @@ def get_pit_stops():
|
||||
user['vehicles'].append(v)
|
||||
|
||||
return render_template('pitstops.html', user=user)
|
||||
|
||||
|
||||
@app.route('/pitstops/plan/vehicle/select', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def select_vehicle_for_plan_pitstop():
|
||||
if len(current_user.vehicles) == 1:
|
||||
return redirect(url_for('select_consumable_for_plan_pitstop', vid=current_user.vehicles[0].id))
|
||||
|
||||
form = SelectVehicleForm()
|
||||
form.vehicle.choices = [(g.id, g.name) for g in current_user.vehicles]
|
||||
|
||||
if form.validate_on_submit():
|
||||
return redirect(url_for('select_consumable_for_plan_pitstop', vid=form.vehicle.data))
|
||||
|
||||
return render_template('selectVehicle.html', form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/plan/vehicle/<int:vid>/consumable/select', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def select_consumable_for_plan_pitstop(vid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('select_consumable_for_plan_pitstop'))
|
||||
|
||||
if len(vehicle.consumables) == 0:
|
||||
flash('Please choose at least one consumable!', 'warning')
|
||||
return redirect(url_for('edit_vehicle', vid=vid))
|
||||
|
||||
if len(vehicle.consumables) == 1:
|
||||
return redirect(url_for('plan_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('plan_pit_stop_form', vid=vid, cid=form.consumable.data))
|
||||
|
||||
return render_template('selectConsumableForVehicle.html', vehicle=vehicle, form=form)
|
||||
|
||||
|
||||
@app.route('/pitstops/plan/vehicle/<int:vid>/consumable/<int:cid>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def plan_pit_stop_form(vid, cid):
|
||||
vehicle = Vehicle.query.get(vid)
|
||||
if vehicle is None or vehicle not in current_user.vehicles:
|
||||
return redirect(url_for('select_vehicle_for_new_pitstop'))
|
||||
|
||||
consumable = Consumable.query.get(cid)
|
||||
if consumable not in vehicle.consumables:
|
||||
return redirect(url_for('select_consumable_for_new_pitstop', vid=vid))
|
||||
|
||||
update_filling_station_prices([x.id for x in current_user.favourite_filling_stations])
|
||||
|
||||
offers = []
|
||||
for fs in current_user.favourite_filling_stations:
|
||||
offers.append((fs, getattr(fs, consumable.ext_id),))
|
||||
|
||||
return render_template('planPitStopForm.html', vehicle=vehicle, consumable=consumable, offers=offers)
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ body {
|
||||
|
||||
.starter-template {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 60px;
|
||||
// padding-bottom: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -92,4 +92,35 @@ and (max-device-width : 568px) {
|
||||
#charts_tabs-content {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filling_station_info {
|
||||
margin: 5px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.filling_station_info img{
|
||||
margin-top: 6px;
|
||||
height:48px;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
.filling_station_closed {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
app/static/img/down.gif
Normal file
|
After Width: | Height: | Size: 54 B |
|
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 980 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
app/static/img/filling_station_favourite_marker.png
Normal file
|
After Width: | Height: | Size: 725 B |
BIN
app/static/img/filling_station_marker.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/static/img/up.gif
Normal file
|
After Width: | Height: | Size: 54 B |
BIN
app/static/img/updown.gif
Normal file
|
After Width: | Height: | Size: 64 B |
4
app/static/jquery-1.11.2.min.js
vendored
184
app/static/js/fillingstations.js
Normal file
@@ -0,0 +1,184 @@
|
||||
// initially display germany
|
||||
var lat = 50.75653081787912,
|
||||
lon = 9.262980794432847,
|
||||
zoom = 5;
|
||||
|
||||
var map;
|
||||
|
||||
var filling_stations = {};
|
||||
var filling_station_markers;
|
||||
|
||||
query_location = function(updater) {
|
||||
if(navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(function(position) {
|
||||
lat = position.coords.latitude;
|
||||
lon = position.coords.longitude;
|
||||
zoom = 11;
|
||||
if(updater){
|
||||
updater(lat, lon);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
update_map = function() {
|
||||
var lonLat = new OpenLayers.LonLat( lon, lat )
|
||||
.transform(
|
||||
new OpenLayers.Projection("EPSG:4326"), // transform from WGS 1984
|
||||
map.getProjectionObject() // to Spherical Mercator Projection
|
||||
);
|
||||
map.setCenter (lonLat, zoom);
|
||||
}
|
||||
|
||||
load_filling_stations = function() {
|
||||
var url = '/filling_stations?latitude=' + lat + '&longitude='+ lon + '&type=all&radius=5&sort=dist';
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: url,
|
||||
success: function(data) {
|
||||
data.stations.forEach(function(station) {
|
||||
if (!(station.id in filling_stations)) {
|
||||
filling_stations[station.id] = station;
|
||||
filling_stations[station.id].marker = false;
|
||||
}
|
||||
});
|
||||
update_filling_station_markers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clicked_on_filling_station_marker = function(station, marker) {
|
||||
return function(data) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/filling_stations/favourites/toggle/'+station.id,
|
||||
dataType: 'json',
|
||||
timeout: 1000,
|
||||
success: function(data) {
|
||||
if (data.state == 'favourite') {
|
||||
marker.setUrl('/static/img/filling_station_favourite_marker.png');
|
||||
} else {
|
||||
marker.setUrl('/static/img/filling_station_marker.png');
|
||||
}
|
||||
},
|
||||
contentType : 'application/json'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
display_station_information = function(station) {
|
||||
return function(event) {
|
||||
var info = $('#station_info');
|
||||
info.empty();
|
||||
info.addClass('filling_station_info');
|
||||
|
||||
var cell1 = $('<div>', {'class':'col-md-8'})
|
||||
var cell2 = $('<div>', {'class':'col-md-4'})
|
||||
|
||||
var img = $('<img>', {'src': '/static/logos/'+station.brand.toLowerCase()+'.png'});
|
||||
// hide if the brand icon loads with error
|
||||
img.error(function(){
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
var name = $('<div>').text(station.name);
|
||||
|
||||
var street_number = $('<div>').text(station.street + ' ' + station.houseNumber);
|
||||
|
||||
var postcode_place = $('<div>').text(station.postCode + ' ' + station.place);
|
||||
|
||||
info.append(cell1
|
||||
.append(name)
|
||||
.append(street_number)
|
||||
.append(postcode_place))
|
||||
.append(cell2
|
||||
.append(img));
|
||||
|
||||
cell2.height(cell1.height());
|
||||
};
|
||||
}
|
||||
|
||||
update_filling_station_markers = function() {
|
||||
for(id in filling_stations) {
|
||||
var station = filling_stations[id];
|
||||
if(!station.marker) {
|
||||
var lonLat = new OpenLayers.LonLat(station.lng, station.lat)
|
||||
.transform(new OpenLayers.Projection('EPSG:4326'), map.getProjectionObject());
|
||||
if (station.state == 'favourite') {
|
||||
var icon = new OpenLayers.Icon('/static/img/filling_station_favourite_marker.png');
|
||||
} else {
|
||||
var icon = new OpenLayers.Icon('/static/img/filling_station_marker.png');
|
||||
}
|
||||
var marker = new OpenLayers.Marker(lonLat, icon);
|
||||
marker.events.register('click', marker, clicked_on_filling_station_marker(station, marker));
|
||||
marker.events.register('mouseover', null, display_station_information(station));
|
||||
filling_station_markers.addMarker(marker);
|
||||
station.marker = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activate_map = function(map_div_id, button_ids, home_lat, home_long, home_zoom, init_stations) {
|
||||
|
||||
// resize to reasonable height
|
||||
$('#' + map_div_id).css('height',0.75*($('#' + map_div_id).css('width')));
|
||||
|
||||
// init map
|
||||
map = new OpenLayers.Map(map_div_id);
|
||||
map.addLayer(new OpenLayers.Layer.OSM());
|
||||
map.events.register('moveend', null, function(e){
|
||||
var p = e.object.center.clone();
|
||||
var p = p.transform(map.getProjectionObject(), 'EPSG:4326');
|
||||
lon = p.lon;
|
||||
lat = p.lat;
|
||||
zoom = e.object.zoom;
|
||||
});
|
||||
filling_station_markers = new OpenLayers.Layer.Markers('Filling Stations');
|
||||
map.addLayer(filling_station_markers);
|
||||
|
||||
// handle initial / favourite stations
|
||||
filling_stations = init_stations;
|
||||
update_filling_station_markers();
|
||||
|
||||
update_map();
|
||||
if ((home_lat == 0) && (home_long == 0)) {
|
||||
query_location(update_map);
|
||||
} else {
|
||||
lat = home_lat;
|
||||
lon = home_long;
|
||||
zoom = home_zoom;
|
||||
update_map();
|
||||
}
|
||||
|
||||
// get button
|
||||
$('#'+button_ids[0]).click(function(e){
|
||||
load_filling_stations();
|
||||
});
|
||||
// set home button
|
||||
$('#'+button_ids[1]).click(function(e){
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/account/home',
|
||||
data: JSON.stringify({'long': lon, 'lat': lat, 'zoom': zoom}),
|
||||
dataType: 'json',
|
||||
timeout: 1000,
|
||||
contentType : 'application/json'
|
||||
});
|
||||
});
|
||||
// go home button
|
||||
$('#'+button_ids[2]).click(function(e){
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/account/home',
|
||||
dataType: 'json',
|
||||
timeout: 1000,
|
||||
success: function(data) {
|
||||
lat = data.lat;
|
||||
lon = data.long;
|
||||
zoom = data.zoom;
|
||||
update_map();
|
||||
},
|
||||
contentType : 'application/json'
|
||||
});
|
||||
});
|
||||
}
|
||||
4
app/static/js/jquery.tablesorter.min.js
vendored
Normal file
BIN
app/static/logos/agip.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
app/static/logos/aral.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
app/static/logos/avia.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/static/logos/bft.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
app/static/logos/esso.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
app/static/logos/hem.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
app/static/logos/jet.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/static/logos/omv.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
app/static/logos/shell.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
app/static/logos/total.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/static/logos/zg raiffeisen energie.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
11
app/static/modernizr-2.8.3-respond-1.4.2.min.js
vendored
1
app/static/normalize.min.css
vendored
@@ -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}
|
||||
@@ -63,6 +63,32 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Filling Stations</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 olMap" style="height: 400px" id="mapdiv"></div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="btn-group col-md-12" role="group">
|
||||
<button type="button" class="btn btn-default glyphicon glyphicon-home" id="go_home_button" />
|
||||
<button type="button" class="btn btn-default glyphicon glyphicon-screenshot " id="set_home_button" />
|
||||
<button type="button" class="btn btn-default glyphicon glyphicon-download" id="get_button" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="station_info" class="row">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var lat = {{ map_pos[0] or 0 }};
|
||||
var long = {{ map_pos[1] or 0 }};
|
||||
var zoom = {{ map_pos[2] or 0 }};
|
||||
var init_filling_station = JSON.parse({{ fs|tojson }});
|
||||
activate_map('mapdiv', ['get_button', 'set_home_button', 'go_home_button'], lat, long, zoom, init_filling_station);
|
||||
</script>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Account</div>
|
||||
<div class="panel-body">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
{{ render_field_with_errors(form.ext_id) }}
|
||||
{{ render_field_with_errors(form.unit) }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<form class='form-horizontal' method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ render_field_with_errors(form.name) }}
|
||||
{{ render_field_with_errors(form.ext_id) }}
|
||||
{{ render_field_with_errors(form.unit) }}
|
||||
{{ render_field_with_errors(form.submit) }}
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% macro navigation() -%}
|
||||
{% if current_user.email %}
|
||||
<li><a id='plan_pitstop_link' href='{{ url_for('select_vehicle_for_plan_pitstop') }}'>Plan Pitstop</a></li>
|
||||
<li><a id='new_pitstop_link' href='{{ url_for('select_vehicle_for_new_pitstop') }}'>Create Pitstop</a></li>
|
||||
<li><a id='new_service_link' href='{{ url_for('select_vehicle_for_new_service') }}'>Create Service</a></li>
|
||||
<li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li>
|
||||
@@ -106,19 +107,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 +128,17 @@
|
||||
<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>
|
||||
<script src="{{ url_for('static', filename='js/fillingstations.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-inverse navbar-fixed-top">
|
||||
@@ -176,6 +181,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://www.lusiardi.de/impressum/" target="_new">Impressum</a> - <a href="https://www.lusiardi.de/datenschutzerklaerung/" target="_new">Datenschutzerklärung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#
|
||||
<nav class="navbar navbar-inverse navbar-fixed-bottom">
|
||||
<div class="container">
|
||||
|
||||
57
app/templates/planPitStopForm.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{% 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>Plan Pitstop for '{{ vehicle.name }}'</h3>
|
||||
Price comparision for {{ consumable.name }}:
|
||||
<div class="table-responsive">
|
||||
<table id="compare" class="table table-striped table-bordered table-condensed tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filling Station</th>
|
||||
<th>Price/{{ consumable.unit }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for offer in offers %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="row filling_station_info " style="border: 0px">
|
||||
<div class="col-md-8">
|
||||
<div>{{ offer[0].name }}</div>
|
||||
<div>{{ offer[0].street }} {{ offer[0].houseNumber }}</div>
|
||||
<div>{{ offer[0].postCode }} {{ offer[0].place }}</div>
|
||||
</div>
|
||||
<div class="col-md-4" style="height: 60px;">
|
||||
<img src="/static/logos/{{ offer[0].brand|lower }}.png">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if offer[0].open %}
|
||||
{{ offer[1] }} €/{{ consumable.unit }}
|
||||
{% else %}
|
||||
Closed
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#compare").tablesorter({sortList: [[1,0]]});
|
||||
$("img").error(function(){
|
||||
$(this).hide();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="col-md-2" ></div>
|
||||
{% endblock %}
|
||||
99
app/tools.py
@@ -1,7 +1,10 @@
|
||||
from sqlalchemy import or_
|
||||
import requests
|
||||
import logging
|
||||
from datetime import date
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
from .entities import Pitstop
|
||||
from .entities import Pitstop, FillingStation
|
||||
from . import db, app
|
||||
|
||||
|
||||
class ConsumableStats:
|
||||
@@ -34,7 +37,7 @@ class ConsumableStats:
|
||||
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:
|
||||
@@ -104,9 +107,9 @@ 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,10 +121,10 @@ 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
|
||||
|
||||
@@ -150,33 +153,6 @@ 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
|
||||
|
||||
@@ -188,4 +164,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 update_filling_station_prices(ids):
|
||||
max_age = (datetime.now() - timedelta(minutes=15)).strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
res = db.session. \
|
||||
query(FillingStation). \
|
||||
filter(FillingStation.id.in_(ids)). \
|
||||
filter(or_(FillingStation.last_update == None, FillingStation.last_update < max_age)). \
|
||||
all()
|
||||
|
||||
if len(res) > 0:
|
||||
id_map = {x.id: x for x in res}
|
||||
query_ids = [x.id for x in res]
|
||||
api_key = app.config['TANKERKOENIG_API_KEY']
|
||||
url = 'https://creativecommons.tankerkoenig.de/json/prices.php'
|
||||
|
||||
# documentation tells us to query max 10 filling stations at a time...
|
||||
for c in chunks(query_ids, 10):
|
||||
params = {
|
||||
'apikey': api_key, 'ids': ','.join(c)
|
||||
}
|
||||
response = requests.get(url, params=params)
|
||||
response_json = response.json()
|
||||
if response_json['ok']:
|
||||
print(response_json)
|
||||
prices = response_json['prices']
|
||||
for price in prices:
|
||||
id = price
|
||||
station_status = prices[id]
|
||||
id_map[id].open = station_status['status'] == 'open'
|
||||
if id_map[id].open:
|
||||
id_map[id].diesel = station_status['diesel']
|
||||
id_map[id].e10 = station_status['e10']
|
||||
id_map[id].e5 = station_status['e5']
|
||||
id_map[id].last_update = datetime.now()
|
||||
else:
|
||||
logging.error(
|
||||
'could not update filling stations because of {r} on URL {u}.'.format(r=str(response_json),
|
||||
u=response.url))
|
||||
db.session.commit()
|
||||
|
||||
16
config.py
@@ -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:///' + os.path.join(basedir, '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://root:{h}@database/pitstops'.format(
|
||||
h=os.environ.get('DATABASE_ENV_MYSQL_ROOT_PASSWORD'))
|
||||
|
||||
|
||||
config = {
|
||||
@@ -44,4 +50,4 @@ config = {
|
||||
'testing': TestingConfig,
|
||||
'production': ProductionConfig,
|
||||
'default': DevelopmentConfig
|
||||
}
|
||||
}
|
||||
|
||||
39
database_upgrades/upgrade_gillian_to_henni.sql
Normal file
@@ -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;
|
||||
|
||||
7
main.py
@@ -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')
|
||||
|
||||
@@ -4,3 +4,5 @@ Flask-Security
|
||||
Flask-WTF
|
||||
PyMySQL
|
||||
markdown
|
||||
Flask-Limiter
|
||||
requests
|
||||