15 Commits

Author SHA1 Message Date
8b78dbe5b7 add DEBUG = False to default config 2017-11-25 11:12:16 +01:00
95a7e56a32 load openlayers via https 2017-11-25 11:11:55 +01:00
9eb4d5ada0 fixup! preparation commit for henni 2017-11-24 09:08:37 +01:00
f7cb273254 preparation commit for henni
This commit includes:
* sql conversion script
* fixes for the column type for lat and lng fields
* removes prints / console logs
* unneccessary comments
* if no home data is available, we center to germany
* hide non working brand logos
* on closed stations the word "closed" is printed instead of outdated
price
* updates on filling stations is done in chunks of max 10
* configuration separates between development and testing
2017-11-24 09:08:04 +01:00
543d3e1658 react to closed stations 2017-11-12 22:50:25 +01:00
9427ed50ad add page to plan a pitstop 2017-11-12 15:38:32 +01:00
28990f27fa moved static files to separate directories 2017-11-12 15:34:49 +01:00
d55fd3d9f6 Consumables can now be linked to the tankerkoenig api 2017-11-12 10:17:36 +01:00
e9bb7986f4 moves routes for filling stations to extra file 2017-11-12 09:44:51 +01:00
eaacd3f42e adds functionality to handle favourit filling stations 2017-11-10 10:38:15 +01:00
8a99e4a616 adds icons & logos
adds icons for filling stations and favorites
adds logos for various brands
2017-11-10 10:37:39 +01:00
028b52d12f Adds setting home in account 2017-11-07 22:33:14 +01:00
958a9bdd9f DEBUG flag can now be set via config and environment 2017-11-07 22:31:31 +01:00
b02938cafe Moves MySQL DB to production 2017-11-07 22:30:44 +01:00
47e81c7517 start implementing a proxy layer for the API of creativecommons.tankerkoenig.de 2017-10-30 06:49:56 +01:00
54 changed files with 682 additions and 86 deletions

View File

@@ -1,12 +1,13 @@
from flask import Flask from flask import Flask, make_response
from flask import g from flask import g
from flask_mail import Mail from flask_mail import Mail
from flask_security import Security, SQLAlchemyUserDatastore, user_registered from flask_security import Security, SQLAlchemyUserDatastore, user_registered
from flask_security.forms import LoginForm
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
import os import os
from config import config 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 * from .forms import *
@@ -14,6 +15,19 @@ from .forms import *
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(config[os.getenv('FLASK_CONFIG') or 'default']) 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) db = SQLAlchemy(app)
mail = Mail(app) mail = Mail(app)
@@ -47,9 +61,21 @@ def user_registered_sighandler(application, user, confirm_token):
tools.db_log_add(new_vehicle) 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 @app.before_first_request
def before_first_request(): def before_first_request():
db.create_all() 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='admin', description='Role for administrators')
user_datastore.find_or_create_role(name='user', description='Role for all users.') user_datastore.find_or_create_role(name='user', description='Role for all users.')
db.session.commit() db.session.commit()

View File

@@ -10,6 +10,11 @@ vehicles_consumables = db.Table('vehicles_consumables',
db.Column('consumable_id', db.Integer(), db.ForeignKey('consumable.id'))) 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): class Role(db.Model, RoleMixin):
""" """
Entity to handle different roles for users: Typically user and admin exist 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)) password = db.Column(db.String(255))
active = db.Column(db.Boolean()) active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime()) 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( vehicles = db.relationship(
'Vehicle' 'Vehicle'
@@ -43,6 +51,10 @@ class User(db.Model, UserMixin):
secondary=roles_users, secondary=roles_users,
backref=db.backref('users', lazy='dynamic') backref=db.backref('users', lazy='dynamic')
) )
favourite_filling_stations = db.relationship(
'FillingStation',
secondary=users_fillingstations
)
def __repr__(self): def __repr__(self):
return '<User id="%r" email="%r" ' % (self.id, self.email) 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) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True) name = db.Column(db.String(255), unique=True)
ext_id = db.Column(db.String(255))
unit = db.Column(db.String(255)) unit = db.Column(db.String(255))
vehicles = db.relationship( vehicles = db.relationship(
@@ -137,8 +150,9 @@ class Consumable(db.Model):
secondary=vehicles_consumables secondary=vehicles_consumables
) )
def __init__(self, name, unit): def __init__(self, name, ext_id, unit):
self.name = name self.name = name
self.ext_id = ext_id
self.unit = unit self.unit = unit
def __repr__(self): def __repr__(self):
@@ -163,3 +177,32 @@ class Service(db.Model):
def __repr__(self): def __repr__(self):
return '<Service odometer="%r" date="%r" vehicle_id="%r" costs="%r" description="%r">' % \ return '<Service odometer="%r" date="%r" vehicle_id="%r" costs="%r" description="%r">' % \
(self.odometer, self.date, self.vehicle_id, self.costs, self.description) (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

View File

@@ -10,12 +10,14 @@ class SelectConsumableForm(FlaskForm):
class CreateConsumableForm(FlaskForm): class CreateConsumableForm(FlaskForm):
name = StringField('Name', validators=[Length(1, 255)]) name = StringField('Name', validators=[Length(1, 255)])
ext_id = SelectField('Tankerkönig ID', coerce=int)
unit = StringField('Unit', validators=[Length(1, 255)]) unit = StringField('Unit', validators=[Length(1, 255)])
submit = SubmitField(label='Do it!') submit = SubmitField(label='Do it!')
class EditConsumableForm(FlaskForm): class EditConsumableForm(FlaskForm):
name = StringField('Name', validators=[Length(1, 255)]) name = StringField('Name', validators=[Length(1, 255)])
ext_id = SelectField('Tankerkönig ID', coerce=int)
unit = StringField('Unit', validators=[Length(1, 255)]) unit = StringField('Unit', validators=[Length(1, 255)])
submit = SubmitField(label='Do it!') submit = SubmitField(label='Do it!')

View File

@@ -3,3 +3,4 @@ from .admin import *
from .misc import * from .misc import *
from .pitstop import * from .pitstop import *
from .service import * from .service import *
from .filling_stations import *

View File

@@ -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 import login_required
from flask_security.core import current_user from flask_security.core import current_user
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
import json
from ..entities import Vehicle, Consumable from ..entities import Vehicle, Consumable
from ..forms import EditVehicleForm, DeleteVehicleForm, DeleteAccountForm from ..forms import EditVehicleForm, DeleteVehicleForm, DeleteAccountForm
@@ -12,7 +13,12 @@ from .. import app, db, user_datastore
@app.route('/account', methods=['GET']) @app.route('/account', methods=['GET'])
@login_required @login_required
def get_account_page(): 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']) @app.route('/account/vehicle/edit/<int:vid>', methods=['GET', 'POST'])
@@ -37,7 +43,7 @@ def edit_vehicle(vid):
vehicle.name = form.name.data vehicle.name = form.name.data
# we cannot delete consumables where there are pitstops for => report error # we cannot delete consumables where there are pitstops for => report error
vehicle.consumables = [] vehicle.consumables = []
for consumable_id in form.consumables.data: for consumable_id in form.consumables.data:
consumable = Consumable.query.get(consumable_id) consumable = Consumable.query.get(consumable_id)
if consumable is not None: if consumable is not None:
vehicle.consumables.append(consumable) vehicle.consumables.append(consumable)
@@ -129,3 +135,21 @@ def delete_account():
return render_template('deleteAccountForm.html', form=form) 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({})

View File

@@ -22,6 +22,8 @@ def get_admin_page():
@login_required @login_required
def create_consumable(): def create_consumable():
form = CreateConsumableForm() 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 # preinitialize the defaults with potentially existing values from a try before
if form.name.data is not None: if form.name.data is not None:
@@ -30,7 +32,7 @@ def create_consumable():
form.unit.default = form.unit.data form.unit.default = form.unit.data
if form.validate_on_submit(): 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) db.session.add(new_consumable)
try: try:
db.session.commit() db.session.commit()
@@ -70,19 +72,28 @@ def edit_consumable(cid):
return redirect(url_for('get_admin_page')) return redirect(url_for('get_admin_page'))
form = EditConsumableForm() form = EditConsumableForm()
choices = [(0, ''), (1, 'diesel'), (2, 'e5'), (3, 'e10')]
form.ext_id.choices = choices
form.name.default = consumable.name form.name.default = consumable.name
form.unit.default = consumable.unit 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 # preinitialize the defaults with potentially existing values from a try before
if form.name.data is not None: if form.name.data is not None:
form.name.default = form.name.data form.name.default = form.name.data
if form.unit.data is not None: if form.unit.data is not None:
form.unit.default = form.unit.data 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(): if form.validate_on_submit():
consumable.name = form.name.data consumable.name = form.name.data
consumable.unit = form.unit.data consumable.unit = form.unit.data
consumable.ext_id = choices[form.ext_id.data][1]
try: try:
db.session.commit() db.session.commit()
db_log_update(consumable) db_log_update(consumable)

View 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)

View File

@@ -6,9 +6,8 @@ from datetime import date
from ..entities import Vehicle, Consumable, Pitstop from ..entities import Vehicle, Consumable, Pitstop
from ..forms import SelectVehicleForm, SelectConsumableForm, CreatePitstopForm, EditPitstopForm, DeletePitStopForm 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, \ from ..tools import db_log_update, db_log_delete, db_log_add, pitstop_service_key, \
get_latest_pitstop_for_vehicle_and_consumable, compute_lower_limits_for_new_pitstop, pitstop_service_key, \ get_event_line_for_vehicle, update_filling_station_prices
get_event_line_for_vehicle
from .. import app, db from .. import app, db
@@ -182,3 +181,63 @@ def get_pit_stops():
user['vehicles'].append(v) user['vehicles'].append(v)
return render_template('pitstops.html', user=user) 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)

View File

@@ -92,4 +92,35 @@ and (max-device-width : 568px) {
#charts_tabs-content { #charts_tabs-content {
display:none; 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;
} }

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
app/static/img/down.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

View File

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 980 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/static/img/up.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

BIN
app/static/img/updown.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

File diff suppressed because one or more lines are too long

View 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'
});
});
}

File diff suppressed because one or more lines are too long

BIN
app/static/logos/agip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
app/static/logos/aral.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
app/static/logos/avia.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/static/logos/bft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/static/logos/esso.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
app/static/logos/hem.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
app/static/logos/jet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
app/static/logos/omv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
app/static/logos/shell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
app/static/logos/total.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

View File

@@ -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}

View File

@@ -63,6 +63,32 @@
</tbody> </tbody>
</table> </table>
</div> </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 panel-default">
<div class="panel-heading">Account</div> <div class="panel-heading">Account</div>
<div class="panel-body"> <div class="panel-body">

View File

@@ -9,6 +9,7 @@
<form class='form-horizontal' method="POST"> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }} {{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.ext_id) }}
{{ render_field_with_errors(form.unit) }} {{ render_field_with_errors(form.unit) }}
{{ render_field_with_errors(form.submit) }} {{ render_field_with_errors(form.submit) }}
</form> </form>

View File

@@ -9,6 +9,7 @@
<form class='form-horizontal' method="POST"> <form class='form-horizontal' method="POST">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ render_field_with_errors(form.name) }} {{ render_field_with_errors(form.name) }}
{{ render_field_with_errors(form.ext_id) }}
{{ render_field_with_errors(form.unit) }} {{ render_field_with_errors(form.unit) }}
{{ render_field_with_errors(form.submit) }} {{ render_field_with_errors(form.submit) }}
</form> </form>

View File

@@ -1,5 +1,6 @@
{% macro navigation() -%} {% macro navigation() -%}
{% if current_user.email %} {% 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_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_service') }}'>Create Service</a></li>
<li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li> <li><a id='statistics_limk' href='{{ url_for('get_statistics') }}'>Statistics</a></li>
@@ -106,19 +107,19 @@
<title>refuel journal</title> <title>refuel journal</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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='img/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='img/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='img/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='img/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='img/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='img/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='img/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='img/apple-touch-icon-152.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='apple-touch-icon-180.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='android-icon-192x192.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='favicon-32x32.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='favicon-96x96.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='favicon-16x16.png') }}"> <link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', filename='img/favicon-16x16.png') }}">
<!-- Latest compiled and minified CSS --> <!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.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"> <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="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 --> <!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <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/amcharts.js"></script>
<script src="https://www.amcharts.com/lib/3/serial.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="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> </head>
<body> <body>
<nav class="navbar navbar-inverse navbar-fixed-top"> <nav class="navbar navbar-inverse navbar-fixed-top">

View 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 %}

View File

@@ -1,7 +1,10 @@
from sqlalchemy import or_
import requests
import logging 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: class ConsumableStats:
@@ -34,7 +37,7 @@ class ConsumableStats:
self.average_amount.append( self.average_amount.append(
StatsEvent( StatsEvent(
current_ps.date, 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: class VehicleStats:
@@ -104,9 +107,9 @@ def get_latest_pitstop_for_vehicle(vehicle_id):
:param vehicle_id: the id of the vehicle :param vehicle_id: the id of the vehicle
:return: the latest pitstop or None if no pitstop exists :return: the latest pitstop or None if no pitstop exists
""" """
latest_pitstop = Pitstop.query\ latest_pitstop = Pitstop.query \
.filter(Pitstop.vehicle_id == vehicle_id)\ .filter(Pitstop.vehicle_id == vehicle_id) \
.order_by(Pitstop.id.desc())\ .order_by(Pitstop.id.desc()) \
.first() .first()
return latest_pitstop 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 :param consumable_id: the id of the consumable
:return: the latest pitstop or None if no pitstop exists :return: the latest pitstop or None if no pitstop exists
""" """
latest_pitstop_consumable = Pitstop.query\ latest_pitstop_consumable = Pitstop.query \
.filter(Pitstop.vehicle_id == vehicle_id)\ .filter(Pitstop.vehicle_id == vehicle_id) \
.filter(Pitstop.consumable_id == consumable_id)\ .filter(Pitstop.consumable_id == consumable_id) \
.order_by(Pitstop.id.desc())\ .order_by(Pitstop.id.desc()) \
.first() .first()
return latest_pitstop_consumable 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) 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): def pitstop_service_key(x):
return x.odometer, x.date return x.odometer, x.date
@@ -188,4 +164,51 @@ def get_event_line_for_vehicle(vehicle):
for service in vehicle.services: for service in vehicle.services:
data.append(service) data.append(service)
data.sort(key=pitstop_service_key) 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()

View File

@@ -1,4 +1,5 @@
import os import os
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
@@ -18,6 +19,8 @@ class Config:
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMIN_MAIL = 'joachim@lusiardi.de' ADMIN_MAIL = 'joachim@lusiardi.de'
TANKERKOENIG_API_KEY = os.environ.get('TANKERKOENIG_API_KEY')
DEBUG = False
@staticmethod @staticmethod
def init_app(app): def init_app(app):
@@ -27,16 +30,19 @@ class Config:
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
SECURITY_SEND_REGISTER_EMAIL = False SECURITY_SEND_REGISTER_EMAIL = False
DEBUG = True 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): class TestingConfig(Config):
TESTING = True SECURITY_SEND_REGISTER_EMAIL = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data_testing.sqlite') 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): 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 = { config = {
@@ -44,4 +50,4 @@ config = {
'testing': TestingConfig, 'testing': TestingConfig,
'production': ProductionConfig, 'production': ProductionConfig,
'default': DevelopmentConfig 'default': DevelopmentConfig
} }

View 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;

View File

@@ -1,12 +1,17 @@
import os import os
from app import app from app import app
import logging import logging
from config import config
def setup_logging(): def setup_logging():
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
if __name__ == '__main__': 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() setup_logging()
app.run(debug=DEBUG, host='0.0.0.0') app.run(debug=DEBUG, host='0.0.0.0')

View File

@@ -4,3 +4,5 @@ Flask-Security
Flask-WTF Flask-WTF
PyMySQL PyMySQL
markdown markdown
Flask-Limiter
requests