Commit 6f9fea39 authored by Lysander Trischler's avatar Lysander Trischler

Normalize indentation: replace tabs by four spaces

parent d5ed2153
......@@ -12,6 +12,8 @@ Kraftwerk-TODO
* REST-API
* flexiblere Gestaltung für Disziplinen
* Körpergewicht vor und nach den Übungen
* Gewicht der Hanteln
* Standard-Werte in Benutzereinstellungen
+ Körpergewicht vor und nach den Übungen
+ Gewicht der Hanteln
+ Standard-Werte in Benutzereinstellungen
* Parsen der Konfigurationsdatei bei SIGHUP
......@@ -3,1119 +3,1119 @@
if __name__ == "__main__":
import logging
import os.path
import random
import string
import datetime
import re
import hashlib
import locale
locale.setlocale(locale.LC_ALL, 'de_DE.utf8')
import pytz
berlin = pytz.timezone("Europe/Berlin")
import tornado.options
import tornado.database
import tornado.web
from tornado.web import authenticated
import tornadyse
from tornadyse import route
import gpxpy
path = lambda name: os.path.join(os.path.dirname(os.path.dirname(__file__)), name)
def date(dt):
"""
Returns the date of the given datetime object. Date changes do not occur
until 3am.
"""
return (dt.date() - datetime.timedelta(days=1)) if dt.hour < 3 else dt.date()
def distance(dist):
"""
Returns a given distance in meters formatted in full meters or kilometers
depending on the scale.
"""
if abs(dist) < 1000:
return "%d m" % dist
return "%.2f km" % (dist / 1000.)
def utc_to_local(utc_dt):
"""
Converts a given date time in UTC to local time using Europe/Berlin as
timezone. Used for GPX file dates in UTC to be converted to the user's
local timezone (for now Europe/Berlin).
"""
if utc_dt.tzinfo is None:
utc_dt = pytz.UTC.localize(utc_dt)
return utc_dt.astimezone(berlin)
class ColorIterator(object):
def __init__(self, *colors):
self.colors = colors or ["navy", "green", "purple", "orange", "yellow", "red", "black"]
self.index = 0
def next(self):
self.index += 1
if len(self.colors) -1 == self.index:
self.index = 0
return self.colors[self.index]
def __iter__(self):
return self
class BaseHandler(tornadyse.RequestHandler):
@property
def db(self):
return self.application.db
def login(self, username, password):
ldap_user = tornadyse.isobeef_ldap_auth(self, username, password)
if ldap_user:
db_user = self.db.get("""
SELECT username, display_name
FROM users
WHERE username = %s
LIMIT 0, 1
""", username)
# update display name if necessary
if db_user:
if db_user.display_name != ldap_user.display_name:
self.db.execute("""
UPDATE users
SET display_name = %s
WHERE username = %s
LIMIT 1
""", ldap_user.display_name, db_user.username)
else:
# insert user data
self.db.execute("""
INSERT INTO users (username, display_name)
VALUES (%s, %s)
""", ldap_user.username, ldap_user.display_name)
# update user cache
self.application.users[ldap_user.username] = ldap_user
return ldap_user
def get_template_namespace(self):
namespace = tornadyse.RequestHandler.get_template_namespace(self)
namespace.update({
"gpx_url": self.gpx_url,
"today": self.today,
"date": date,
"gpx_url": self.gpx_url,
"distance": distance
})
return namespace
def gpx_url(self, filename):
if "gpx_url_prefix" in self.settings:
gpx_url_prefix = self.settings["gpx_url_prefix"]
else:
gpx_url_prefix = self.settings.get("static_url_prefix", "/static/") + "gpx/"
return gpx_url_prefix + filename
@property
def now(self):
if not hasattr(self, "_now"):
self._now = datetime.datetime.now()
return self._now
@property
def today(self):
"""
Returns the current date but with respect to date changes happening not before 3am.
This result is cached.
"""
if not hasattr(self, "_today"):
self._today = date(self.now)
return self._today
def get_exercise(self, exercise_id, own=True):
exercise = self.db.get("""
SELECT id, username, discipline, count, start, end
FROM exercises
WHERE id = %s
LIMIT 0, 1
""", exercise_id)
if not exercise:
raise tornado.web.HTTPError(404)
if own and exercise.username != self.current_user.username:
raise tornado.web.HTTPError(403)
return Exercise(**exercise)
def get_gpx_file(self, gpx_file_id):
return GPXFile(self, self.db.get("""
SELECT id, filename, exercise_id, start, end
FROM gpx_files
WHERE id = %s
LIMIT 0,1
""", gpx_file_id))
def get_gpx_files(self, exercise_id):
return [GPXFile(self, gpx_file) for gpx_file in self.db.query("""
SELECT id, filename, exercise_id, start, end
FROM gpx_files
WHERE exercise_id = %s
ORDER BY start ASC
""", exercise_id)]
def get_suitable_gpx_files(self, exercise):
return [GPXFile(self, gpx_file) for gpx_file in self.db.query("""
SELECT
gpx_files.id,
gpx_files.filename,
exercises.id as exercise_id,
gpx_files.start,
gpx_files.end,
exercises.username,
exercises.count,
exercises.discipline
FROM gpx_files INNER JOIN exercises ON gpx_files.exercise_id = exercises.id
WHERE %s <= gpx_files.start AND gpx_files.end <= %s
ORDER BY gpx_files.start ASC
""", exercise.start - datetime.timedelta(days=1), exercise.end + datetime.timedelta(days=1))]
class GPXFile(tornado.database.Row):
def __init__(self, handler, data):
super(GPXFile, self).__init__(**data)
self._handler = handler
@property
def gpx(self):
if not hasattr(self, "_gpx"):
with open(os.path.join(self._handler.settings.get("static_path"), "gpx", self["filename"])) as f:
self._gpx = gpxpy.parse(f)
return self._gpx
@property
def exercise(self):
if not hasattr(self, "_exercise"):
self._exercise = Exercise(id=self.exercise_id, username=self.username, count=self.count, discipline=self.discipline)
return self._exercise
@property
def start(self):
return utc_to_local(self["start"])
@property
def end(self):
return utc_to_local(self["end"])
@route(r'/')
class HomeHandler(BaseHandler):
def get(self):
self.render("base.html")
@route(r'/login')
class LoginHandler(tornadyse.LoginHandler, BaseHandler):
login = BaseHandler.login
@route(r'/logout')
class LogoutHandler(tornadyse.LogoutHandler, BaseHandler): pass
@route(r'/exercises', aliases=[r'/list'])
class ListExercisesHandler(BaseHandler):
@authenticated
def get(self):
self.render("exercises.html", exercises=[Exercise(**exercise) for exercise in self.db.query("""
SELECT id, username, discipline, count, start, end
FROM exercises
ORDER BY start DESC, end DESC, count, username
""")])
@route(r'/exercises/add', aliases=[r'/add'])
class AddExerciseHandler(BaseHandler):
def prepare(self):
def format_datetime(datetime):
if "Opera" in self.request.headers.get("User-Agent", ""):
#if datetime.seconds == 0: #or self.current_user.username == "wf":
return datetime.strftime("%Y-%m-%dT%H:%MZ")
#else:
# return datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
return datetime.strftime("%d.%m.%Y %H:%M:%S")
self.format_datetime = format_datetime
@authenticated
def get(self):
exercise = Exercise(user=self.current_user)
self.render("add.html", exercise=exercise, format_datetime=self.format_datetime)
def _parse_datetime(self, text):
if not text:
return None
today = datetime.date.today()
now = datetime.datetime.time(self.now)
def parse_date(date):
if re.match("^\d{2}(\d{2})?-\d{1,2}-\d{1,2}$", date):
year, month, day = date.split("-")
return datetime.date(year=int(year), month=int(month), day=int(day))
if date == "vorvorgestern":
return today - datetime.timedelta(days=3)
if date == "vorgestern":
return today - datetime.timedelta(days=2)
if date in ("yesterday", "gestern"):
return today - datetime.timedelta(days=1)
if date in ("today", "heute"):
return today
if re.match(r"^\d{1,2}$", date):
return today.replace(day=int(date))
if re.match(r"^\d{1,2}\.\d{1,2}\.?$", date):
day, month = date.split(".", 1)
return today.replace(day=int(day), month=int(month.replace(".", "")))
if re.match(r"^\d{1,2}\.\d{1,2}\.\d{4}$", date):
day, month, year = date.split(".")
return today.replace(day=int(day), month=int(month), year=int(year))
return None
def parse_time(time):
if re.match(r"^\d{1,2}$", time):
return now.replace(minute=int(time), second=0)
if re.match(r"^\d{1,2}:\d{1,2}$", time):
hour, minute = time.split(":")
return now.replace(hour=int(hour), minute=int(minute), second=0)
if re.match(r"^\d{1,2}:\d{1,2}:\d{1,2}$", time):
hour, minute, second = time.split(":")
return now.replace(hour=int(hour), minute=int(minute), second=int(second))
return None
parts = text.split()
if len(parts) == 1:
if re.match("^\d{2}(\d{2})?-\d{1,2}-\d{1,2}T\d{1,2}:\d{1,2}(:\d{1,2})?Z$", parts[0]):
date, time = parts[0].split("T")
year, month, day = date.split("-")
time = time[:-1].split(":")
if len(time) == 2:
time += [0]
hour, minute, second = time
return datetime.datetime(year=int(year), month=int(month), day=int(day),
hour=int(hour), minute=int(minute), second=int(second))
# only time given
date = today
time = parse_time(parts[0])
elif len(parts) == 2:
# both date and time given
date = parse_date(parts[0])
time = parse_time(parts[1])
else:
# corrupt data
return None
if date is not None and time is not None:
return datetime.datetime.combine(date, time)
return None
def _parse_duration(self, duration):
if not duration:
return None
duration = duration.lower()
if re.match(r"^\d{1,2}$", duration):
return datetime.timedelta(minutes=int(duration))
if re.match(r"^\d{1,2}:\d{1,2}$", duration):
hours, minutes = duration.split(":")
return datetime.timedelta(hours=int(hours), minutes=int(minutes))
if re.match(r"^\d{1,2}:\d{1,2}:\d{1,2}$", duration):
hours, minutes, seconds = duration.split(":")
return datetime.timedelta(hours=int(hours), minutes=int(minutes), seconds=int(seconds))
def parse_hours(hours):
if re.match(r"^\d{1,2}$", pre):
return datetime.timedelta(hours=int(pre))
if re.match(r"^\d{1,2}:\d{1,2}$", pre):
hours, minutes = pre.split(":")
return datetime.timedelta(hours=int(hours), minutes=int(minutes))
if re.match(r"\d{1,2}:\d{1,2}:\d{1,2}$", pre):
hours, minutes, seconds = pre.split(":")
return datetime.timedelta(hours=int(hours), minutes=int(minutes), seconds=int(seconds))
return None
for abbr in ("hours", "hrs", "h"):
if abbr in duration:
pre, post = duration.split(abbr, 1)
if post == "":
return parse_hours(pre)
# FIXME x min y sec z hours or sth. similar
return None
def parse_minutes(minutes):
if re.match(r"^\d{1,2}$", pre):
return datetime.timedelta(minutes=int(pre))
if re.match(r"^\d{1,2}:\d{1,2}$", pre):
minutes, seconds = pre.split(":")
return datetime.timedelta(minutes=int(minutes), seconds=int(seconds))
return None
for abbr in ("mins", "min", "m"):
if abbr in duration:
pre, post = duration.split(abbr, 1)
if post == "":
return parse_minutes(pre)
# FIXME x min y sec z hours or sth. similar
return None
def parse_seconds(seconds):
if re.match(r"^\d{1,2}$", seconds):
return datetime.timedelta(seconds=int(seconds))
return None
for abbr in ("secs", "sec", "s"):
if abbr in duration:
pre, post = duration.split(abbr, 1)
if post == "":
return parse_seconds(pre)
# FIXME x min y sec z hours or sth. similar
return None
return None
@authenticated
def post(self):
#
# get all the data and check them
#
# discipline
discipline = self.get_string_argument("discipline", None) \
or self.get_string_argument("other_discipline", None)
if not discipline:
self.error_messages.append("Bitte eine Disziplin wählen!")
self.set_status(400)
# exercises count
count = self.get_int_argument("count", None)
if count is None or count < 0 or count > 9001:
self.error_messages.append("Bitte eine gültige Übungsanzahl zwischen 0 und 9001 wählen!")
self.set_status(400)
# exercise start date
start = self._parse_datetime(self.get_string_argument("start", None))
if start is None:
self.error_messages.append("Bitte einen gültigen Übungsbeginnzeitpunkt wählen!")
self.set_status(400)
# exercise end date
end = self._parse_datetime(self.get_string_argument("end", None))
if end is None:
# exercise duration
duration = self._parse_duration(self.get_string_argument("duration", None))
if duration is not None:
if start:
end = start + duration
else:
self.error_messages.append("Aufgrund der fehlerhaften Startzeit kann mittels der Dauer keine Endzeit bestimmt werden!")
self.set_status(400)
else:
self.error_messages.append("Bitte einen gültigen Übungsendezeitpunkt wählen!")
self.set_status(400)
if start and end:
# end cannot be before start
if end < start:
self.error_messages.append("Das Enddatum darf nicht vor dem Startdatum liegen!")
self.set_status(400)
# end cannot be more than one week
if end > self.now + datetime.timedelta(days=7):
self.error_messages.append("Das Enddatum liegt zu weit in der Zukunft!")
self.set_status(400)
# start cannot be before 1970-01-01 00:00:00
if start < datetime.datetime(year=1970, month=1, day=1):
self.error_messages.append("Das Startdatum liegt zu weit in der Vergangenheit!")
self.set_status(400)
exercise = Exercise(user=self.current_user,
discipline=discipline,
count=count,
start=start,
end=end)
if self.get_status() != 200:
self.render("add.html", exercise=exercise, format_datetime=self.format_datetime)
else:
exercise_id = self.db.execute_lastrowid("""
INSERT INTO exercises (username, discipline, count, start, end)
VALUES (%s, %s, %s, %s, %s)
""", exercise.user.username, exercise.discipline, exercise.count, exercise.start, exercise.end)
self.success_messages.append("Übung erfolgreich eingetragen!")
self.redirect(self.reverse_url("ViewExerciseHandler", exercise_id))
@route(r'/remove/(\d+)')
class RemoveExerciseHandler(BaseHandler):
@authenticated
def get(self, exercise_id):
self.render("remove.html", exercise=self.get_exercise(exercise_id))
@authenticated
def post(self, exercise_id):
if self.get_string_argument("cancel", None) is None:
#
# remove the exercise from DB
#
rowcount = self.db.execute_rowcount("""
DELETE FROM exercises
WHERE id = %s
LIMIT 1
""", self.get_exercise(exercise_id).id)
if rowcount == 0:
# someone already removed the exercise between the get_exercise() and
# execute_rowcount() method calls
raise tornado.web.HTTPError(410)
#
# remove GPX files from filesystem if not referenced any more
#
# find referenced GPX files in this exercise
for gpx_file in self.db.query("SELECT filename FROM gpx_files WHERE exercise_id = %s", exercise_id):
# check if referenced somewhere else
if self.db.get("SELECT COUNT(filename) AS count FROM gpx_files WHERE filename = %s", gpx_file.filename).count == 1:
gpx_filename = os.path.abspath(os.path.join(self.settings["static_path"], "gpx", gpx_file.filename))
logging.info("Unlinking %s because it's unreferenced..." % gpx_filename)
try:
os.unlink(gpx_filename)
except Exception as e:
logging.error(e)
#
# remove GPX files from DB
#
rowcount = self.db.execute_rowcount("""
DELETE FROM gpx_files
WHERE exercise_id = %s
""", exercise_id)
self.success_messages.append("Die Übung" + ((" und GPS-Trajektorie" + ("" if rowcount == 1 else "n")) if rowcount > 0 else "") + " wurde" + ("" if rowcount == 1 else "n") + " erfolgreich gelöscht.")
self.redirect()
@route(r'/exercises/(\d+)')
class ViewExerciseHandler(BaseHandler):
@authenticated
def get(self, exercise_id):
self.render("view_exercise.html",
exercise=self.get_exercise(exercise_id, own=False),
gpx_files=self.get_gpx_files(exercise_id),
colors=ColorIterator())
@route(r'/exercises/add_gpx/(\d+)')
class AddGPXHandler(BaseHandler):
@authenticated
def get(self, exercise_id):
exercise = self.get_exercise(exercise_id)
self.render("add_gpx.html",
exercise=exercise,
gpx_files=self.get_suitable_gpx_files(exercise=exercise))
@authenticated
def post(self, exercise_id):
if self.get_string_argument("cancel", None) is None:
exercise = self.get_exercise(exercise_id)
if "gpx" not in self.request.files:
#
# handle existing GPX file assignment
#
gpx_file_id = self.get_int_argument("gpx_file_id", None)
if gpx_file_id is None:
self.set_status(400)
self.error_messages.append("Keine GPX-Datei angegeben und keine bestehende GPS-Trajektorie ausgewählt!")
self.render("add_gpx.html",
exercise=exercise,
gpx_files=self.get_suitable_gpx_files(exercise=exercise))
return
else:
# check for valid GPX file ID
gpx_file = self.get_gpx_file(gpx_file_id=gpx_file_id)
if gpx_file is None:
self.set_status(400)
self.error_messages.append("Die angegebene GPS-Trajektorie existiert nicht!")
self.render("add_gpx.html",
exercise=exercise,
gpx_files=self.get_suitable_gpx_files(exercise=exercise))
return
else:
# assign exercise given GPX file if not already attached to this exercise
if self.db.query("""
SELECT id
FROM gpx_files
WHERE exercise_id = %s AND filename = %s
""", exercise_id, gpx_file.filename):
self.set_status(400)
self.error_messages.append("Diese Datei wurde zu dieser Übung bereits schon einmal angegeben!")
self.render("add_gpx.html",
exercise=exercise,
gpx_files=self.get_suitable_gpx_files(exercise=exercise))
return
else:
self.db.execute("""
INSERT INTO gpx_files
(filename, exercise_id, start, end)
VALUES (%s, %s, %s, %s)
""", gpx_file.filename, exercise_id, gpx_file.start.astimezone(pytz.UTC), gpx_file.end.astimezone(pytz.UTC))
self.success_messages.append("GPS-Trajektorie erfolgreich der Übung hinzugefügt!")
else:
#
# handle new GPX file upload
#
gpx = self.request.files['gpx'][0]["body"]
filename = hashlib.sha1(gpx).hexdigest() + ".gpx"
try:
g = gpxpy.parse(gpx)
except Exception as e: