From 910ee8ffcd43add45e53a5d3fbe63193cd4f6fcb Mon Sep 17 00:00:00 2001 From: Stefan Hackenberg Date: Sat, 27 Jul 2024 15:24:35 +0200 Subject: [PATCH] Implement better stats --- coffeebuddy/__init__.py | 2 +- coffeebuddy/model.py | 110 +++++++++++++++++++++++++++--- coffeebuddy/route_chart.py | 40 +---------- coffeebuddy/route_coffee.py | 2 + coffeebuddy/templates/coffee.html | 23 +------ coffeebuddy/templates/stats.html | 45 ++++++++---- 6 files changed, 138 insertions(+), 84 deletions(-) diff --git a/coffeebuddy/__init__.py b/coffeebuddy/__init__.py index abd8ebb..a8199da 100644 --- a/coffeebuddy/__init__.py +++ b/coffeebuddy/__init__.py @@ -44,7 +44,7 @@ def create_app(config=None): else: logging.getLogger(__name__).info('Using config file "config"') app.config.from_object("config") - # app.config['SQLALCHEMY_ECHO'] = True + # app.config["SQLALCHEMY_ECHO"] = True if config: app.config.update(config) diff --git a/coffeebuddy/model.py b/coffeebuddy/model.py index fb22ddd..53ca9ff 100644 --- a/coffeebuddy/model.py +++ b/coffeebuddy/model.py @@ -1,12 +1,34 @@ -import datetime +import calendar import socket -from typing import Optional +from datetime import date, datetime, timedelta +from typing import List, Optional, Tuple import flask import sqlalchemy from sqlalchemy import text +def db_weekday(column): + """Helper to extract weekday for different database backends""" + if flask.current_app.db.engine.name == "postgresql": + return sqlalchemy.func.extract("dow", column) + else: + return sqlalchemy.func.strftime("%w", column) + + +def weekday(number): + """ + Helper to return the name of the weekday for given day number. + 0: Sunday + 1: Monday + .. + """ + if number == 0: + return calendar.day_name[6] + else: + return calendar.day_name[number - 1] + + class Serializer: @staticmethod def escape(obj): @@ -58,9 +80,7 @@ def by_tag(tag): def drinks_today(self): return ( Drink.query.filter(Drink.user == self) - .filter( - flask.current_app.db.func.Date(Drink.timestamp) == datetime.date.today() - ) + .filter(flask.current_app.db.func.Date(Drink.timestamp) == date.today()) .all() ) @@ -131,6 +151,80 @@ def count_selected_manually(self, host: Optional[str] = None) -> int: host = host or socket.gethostname() return sum(d.selected_manually for d in self.drinks if d.host == host) + def drinks_this_week(self) -> Tuple[List[str], List[int]]: + db = flask.current_app.db + now = date.today() + start_of_week = datetime.combine( + now - timedelta(now.weekday()), datetime.min.time() + ) + + data = tuple( + zip( + *db.session.execute( + db.select( + db_weekday(Drink.timestamp).label("weekday"), + db.func.count(db.func.Date(Drink.timestamp)), + ) + .where(Drink.userid == self.id) + .where(Drink.timestamp >= start_of_week) + .group_by("weekday") + .order_by("weekday") + ).all() + ) + ) + + if not data: + return [], [] + return [weekday(int(i)) for i in data[0]], list(data[1]) + + def drinks_avg_week(self) -> Tuple[List[str], List[int]]: + db = flask.current_app.db + + number_of_weeks = ( + datetime.now() + - db.session.scalars( + db.select(db.func.min(Drink.timestamp)).filter_by(userid=self.id) + ).first() + ).days / 7 + data = tuple( + zip( + *db.session.execute( + db.select( + db_weekday(Drink.timestamp).label("weekday"), + db.func.count(db.func.Date(Drink.timestamp)) / number_of_weeks, + ) + .where(Drink.userid == self.id) + .group_by("weekday") + .order_by("weekday") + ).all() + ) + ) + return [weekday(int(i)) for i in data[0]], list(data[1]) + + @staticmethod + def drinks_avg_week_all() -> Tuple[List[str], List[int]]: + db = flask.current_app.db + + number_of_weeks = ( + datetime.now() - db.session.scalars(db.func.min(Drink.timestamp)).first() + ).days / 7 + number_of_active_users = User.query.filter(User.enabled).count() + data = tuple( + zip( + *db.session.execute( + db.select( + db_weekday(Drink.timestamp).label("weekday"), + db.func.count(db.func.Date(Drink.timestamp)) + / number_of_weeks + / number_of_active_users, + ) + .group_by("weekday") + .order_by("weekday") + ).all() + ) + ) + return [weekday(int(i)) for i in data[0]], list(data[1]) + class Drink(flask.current_app.db.Model): id = flask.current_app.db.Column(flask.current_app.db.Integer, primary_key=True) @@ -148,7 +242,7 @@ class Drink(flask.current_app.db.Model): def __init__(self, *args, **kwargs): if "timestamp" not in kwargs: - kwargs["timestamp"] = datetime.datetime.now() + kwargs["timestamp"] = datetime.now() if "host" not in kwargs: kwargs["host"] = socket.gethostname() super().__init__(*args, **kwargs) @@ -162,7 +256,7 @@ def drinks_vs_days(timedelta): ), flask.current_app.db.func.Date(Drink.timestamp), ) - .filter(Drink.timestamp > datetime.datetime.now() - timedelta) + .filter(Drink.timestamp > datetime.now() - timedelta) .order_by(sqlalchemy.asc(flask.current_app.db.func.Date(Drink.timestamp))) .group_by(flask.current_app.db.func.Date(Drink.timestamp)) .all() @@ -184,7 +278,7 @@ class Pay(flask.current_app.db.Model): def __init__(self, *args, **kwargs): if "timestamp" not in kwargs: - kwargs["timestamp"] = datetime.datetime.now() + kwargs["timestamp"] = datetime.now() if "host" not in kwargs: kwargs["host"] = socket.gethostname() super().__init__(*args, **kwargs) diff --git a/coffeebuddy/route_chart.py b/coffeebuddy/route_chart.py index 138f882..e20bb38 100644 --- a/coffeebuddy/route_chart.py +++ b/coffeebuddy/route_chart.py @@ -3,22 +3,6 @@ from coffeebuddy.model import User, escapefromhex -class Color: - def __init__(self, r, g, b): - self.r = r - self.g = g - self.b = b - - def __str__(self): - return f"rgb({self.r}, {self.g}, {self.b})" - - def brighter(self, factor): - r = self.r + (255 - self.r) * factor - g = self.g + (255 - self.g) * factor - b = self.b + (255 - self.b) * factor - return Color(r, g, b) - - def init(): @flask.current_app.route("/stats.html", methods=["GET", "POST"]) def chart(): @@ -33,26 +17,4 @@ def chart(): return flask.redirect(f'coffee.html?tag={flask.request.args["tag"]}') return flask.redirect("/") - berry = Color(171, 55, 122) - - x = list(user.drink_days) - n = user.max_drinks_per_day - datasets = [ - { - "x": x, - "y": [ - f"1970-01-01T{user.nth_drink(date, i).timestamp.time().isoformat()}" - for date in x - ], - "fill": "tozeroy", - "name": f"{i}. Coffee", - "mode": "markers", - "fillcolor": str(berry.brighter(1 - i / n)), - "line": { - "color": str(berry), - }, - } - for i in range(n, 0, -1) - ] - - return flask.render_template("stats.html", user=user, datasets=datasets) + return flask.render_template("stats.html", user=user) diff --git a/coffeebuddy/route_coffee.py b/coffeebuddy/route_coffee.py index a9e16e0..f7b7977 100644 --- a/coffeebuddy/route_coffee.py +++ b/coffeebuddy/route_coffee.py @@ -8,6 +8,8 @@ def init(): def coffee(): flask.current_app.events.fire("route_coffee") user: User = User.by_tag(escapefromhex(flask.request.args["tag"])) + print("drinks_this_week", user.drinks_this_week()) + print("drinks_avg_week", user.drinks_avg_week()) if user is None: return flask.render_template( "cardnotfound.html", uuid=flask.request.args["tag"] diff --git a/coffeebuddy/templates/coffee.html b/coffeebuddy/templates/coffee.html index c3fdf00..507a950 100644 --- a/coffeebuddy/templates/coffee.html +++ b/coffeebuddy/templates/coffee.html @@ -109,7 +109,6 @@ width: 300px; height: 300px; } - @@ -157,6 +156,7 @@

{{ user.prename }} {{ user.name }}

{{ hexstr(user.tag) }}
+
-
-
-
- COFFEEMETER -
-
- {{ len(user.drinks_today) }} -
-
-
-
-
-
-
-
-
-
+
diff --git a/coffeebuddy/templates/stats.html b/coffeebuddy/templates/stats.html index 3f53a22..3da9557 100644 --- a/coffeebuddy/templates/stats.html +++ b/coffeebuddy/templates/stats.html @@ -6,7 +6,7 @@ - +