Compare commits
6 Commits
f13ed8a078
...
1.3.0
Author | SHA1 | Date | |
---|---|---|---|
4552cf7616 | |||
a614b51d29 | |||
e67aa3fda1 | |||
8423fd02b4 | |||
2bd07e5f2d | |||
058b83522c |
@ -1,9 +1,12 @@
|
|||||||
## Unreleased
|
## 1.3.0 / 2023-11-05 15:09+01:00
|
||||||
|
|
||||||
### New
|
### New
|
||||||
* Add Stats to the main navigation
|
* Add Stats to the main navigation
|
||||||
* Allow selecting year on the Stats page
|
* Allow selecting year on the Stats page
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Make some pages redirect back instead to session list
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
* Make navigation more compact
|
* Make navigation more compact
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ RUN npm install && \
|
|||||||
|
|
||||||
FROM python:3.10.9-slim-bullseye
|
FROM python:3.10.9-slim-bullseye
|
||||||
|
|
||||||
ENV VERSION_NUMBER 1.2.0
|
ENV VERSION_NUMBER 1.3.0
|
||||||
ENV PROD 1
|
ENV PROD 1
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
@ -10,23 +10,30 @@
|
|||||||
<form method="get" class="text-center">
|
<form method="get" class="text-center">
|
||||||
<label class="text-5xl text-center inline-block mb-10" for="yearSelect">Stats for:</label>
|
<label class="text-5xl text-center inline-block mb-10" for="yearSelect">Stats for:</label>
|
||||||
<select name="year" id="yearSelect" onchange="this.form.submit();" class="mx-2">
|
<select name="year" id="yearSelect" onchange="this.form.submit();" class="mx-2">
|
||||||
<option value="2022" {% if year == 2022 %}selected{% endif %}>2022</option>
|
{% for year_item in stats_dropdown_year_range %}
|
||||||
<option value="2023" {% if year == 2023 %}selected{% endif %}>2023</option>
|
<option value="{{ year_item }}" {% if year == year_item %}selected{% endif %}>{{ year_item }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<table class="responsive-table">
|
<table class="responsive-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Total hours</th>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Hours</th>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Total games</th>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Games</th>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Released that year</th>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Purchases</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">{{ total_spent_currency }}/game</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_hours }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_hours }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_purchased_this_year.count }}</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }}</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ spent_per_game }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -39,14 +46,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for purchase in top_10_by_playtime %}
|
{% for game in top_10_games_by_playtime %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a href="{% url 'view_game' purchase.edition.game.id %}">{{ purchase.edition.name }}
|
<a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'view_game' game.id %}">{{ game.name }}
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.formatted_playtime }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ game.formatted_playtime }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -62,11 +69,30 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for item in total_playtime_per_platform %}
|
{% for item in total_playtime_per_platform %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.platform_name }} </td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.platform_name }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.formatted_playtime }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.formatted_playtime }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h1 class="text-5xl text-center my-6">Purchases</h1>
|
||||||
|
<table class="responsive-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for purchase in all_purchased_this_year %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"><a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">{{ purchase.edition.name }}</a></td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
100
games/views.py
100
games/views.py
@ -1,11 +1,11 @@
|
|||||||
from common.time import format_duration
|
from common.time import format_duration, now as now_with_tz
|
||||||
from common.time import now as now_with_tz
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Sum, F
|
from django.db.models import Sum, F
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from typing import Callable, Any
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@ -30,7 +30,7 @@ def model_counts(request):
|
|||||||
|
|
||||||
|
|
||||||
def stats_dropdown_year_range(request):
|
def stats_dropdown_year_range(request):
|
||||||
return {"stats_dropdown_year_range": range(2022, 2024)}
|
return {"stats_dropdown_year_range": range(2018, 2024)}
|
||||||
|
|
||||||
|
|
||||||
def add_session(request):
|
def add_session(request):
|
||||||
@ -61,6 +61,25 @@ def update_session(request, session_id=None):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
|
def use_custom_redirect(
|
||||||
|
func: Callable[..., HttpResponse]
|
||||||
|
) -> Callable[..., HttpResponse]:
|
||||||
|
"""
|
||||||
|
Will redirect to "return_path" session variable if set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrapper(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
|
response = func(request, *args, **kwargs)
|
||||||
|
if isinstance(response, HttpResponseRedirect) and (
|
||||||
|
next_url := request.session.get("return_path")
|
||||||
|
):
|
||||||
|
return HttpResponseRedirect(next_url)
|
||||||
|
return response
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def edit_session(request, session_id=None):
|
def edit_session(request, session_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
session = Session.objects.get(id=session_id)
|
session = Session.objects.get(id=session_id)
|
||||||
@ -73,6 +92,7 @@ def edit_session(request, session_id=None):
|
|||||||
return render(request, "add_session.html", context)
|
return render(request, "add_session.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def edit_purchase(request, purchase_id=None):
|
def edit_purchase(request, purchase_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
purchase = Purchase.objects.get(id=purchase_id)
|
purchase = Purchase.objects.get(id=purchase_id)
|
||||||
@ -85,6 +105,7 @@ def edit_purchase(request, purchase_id=None):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def edit_game(request, game_id=None):
|
def edit_game(request, game_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
purchase = Game.objects.get(id=game_id)
|
purchase = Game.objects.get(id=game_id)
|
||||||
@ -119,9 +140,11 @@ def view_game(request, game_id=None):
|
|||||||
context["last_session"] = context["sessions"].first()
|
context["last_session"] = context["sessions"].first()
|
||||||
context["first_session"] = context["sessions"].last()
|
context["first_session"] = context["sessions"].last()
|
||||||
context["sessions_with_notes"] = context["sessions"].exclude(note="")
|
context["sessions_with_notes"] = context["sessions"].exclude(note="")
|
||||||
|
request.session["return_path"] = request.path
|
||||||
return render(request, "view_game.html", context)
|
return render(request, "view_game.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def edit_platform(request, platform_id=None):
|
def edit_platform(request, platform_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
purchase = Platform.objects.get(id=platform_id)
|
purchase = Platform.objects.get(id=platform_id)
|
||||||
@ -134,6 +157,7 @@ def edit_platform(request, platform_id=None):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def edit_edition(request, edition_id=None):
|
def edit_edition(request, edition_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
edition = Edition.objects.get(id=edition_id)
|
edition = Edition.objects.get(id=edition_id)
|
||||||
@ -146,6 +170,7 @@ def edit_edition(request, edition_id=None):
|
|||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@use_custom_redirect
|
||||||
def start_game_session(request, game_id: int):
|
def start_game_session(request, game_id: int):
|
||||||
last_session = (
|
last_session = (
|
||||||
Session.objects.filter(purchase__edition__game_id=game_id)
|
Session.objects.filter(purchase__edition__game_id=game_id)
|
||||||
@ -243,29 +268,42 @@ def stats(request, year: int = 0):
|
|||||||
year = now_with_tz().year
|
year = now_with_tz().year
|
||||||
first_day_of_year = datetime(year, 1, 1)
|
first_day_of_year = datetime(year, 1, 1)
|
||||||
last_day_of_year = datetime(year + 1, 1, 1)
|
last_day_of_year = datetime(year + 1, 1, 1)
|
||||||
year_sessions = Session.objects.filter(
|
year_sessions = Session.objects.filter(timestamp_start__year=year)
|
||||||
timestamp_start__gte=first_day_of_year
|
year_played_purchases = Purchase.objects.filter(
|
||||||
).filter(timestamp_start__lt=last_day_of_year)
|
session__in=year_sessions
|
||||||
year_purchases = Purchase.objects.filter(session__in=year_sessions).distinct()
|
).distinct()
|
||||||
year_purchases_with_playtime = year_purchases.annotate(
|
|
||||||
total_playtime=Sum(
|
selected_currency = "CZK"
|
||||||
F("session__duration_calculated") + F("session__duration_manual")
|
all_purchased_this_year = (
|
||||||
)
|
Purchase.objects.filter(date_purchased__year=year)
|
||||||
|
.filter(price_currency__exact=selected_currency)
|
||||||
|
.filter(date_refunded__exact=None)
|
||||||
|
.order_by("date_purchased")
|
||||||
)
|
)
|
||||||
top_10_by_playtime = year_purchases_with_playtime.order_by("-total_playtime")[:10]
|
|
||||||
for purchase in top_10_by_playtime:
|
this_year_spendings = all_purchased_this_year.aggregate(total_spent=Sum(F("price")))
|
||||||
purchase.formatted_playtime = format_duration(purchase.total_playtime, "%2.0H")
|
total_spent = this_year_spendings["total_spent"]
|
||||||
|
|
||||||
|
games_with_playtime = (
|
||||||
|
Game.objects.filter(edition__purchase__session__in=year_sessions)
|
||||||
|
.annotate(
|
||||||
|
total_playtime=Sum(
|
||||||
|
F("edition__purchase__session__duration_calculated")
|
||||||
|
+ F("edition__purchase__session__duration_manual")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values("id", "name", "total_playtime")
|
||||||
|
)
|
||||||
|
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")[:10]
|
||||||
|
for game in top_10_games_by_playtime:
|
||||||
|
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
|
||||||
|
|
||||||
total_playtime_per_platform = (
|
total_playtime_per_platform = (
|
||||||
year_sessions.values("purchase__platform__name") # Group by platform name
|
year_sessions.values("purchase__platform__name")
|
||||||
.annotate(
|
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
||||||
total_playtime=Sum(F("duration_calculated") + F("duration_manual"))
|
.annotate(platform_name=F("purchase__platform__name"))
|
||||||
) # Sum the duration_calculated for each group
|
.values("platform_name", "total_playtime")
|
||||||
.annotate(platform_name=F("purchase__platform__name")) # Rename the field
|
.order_by("-total_playtime")
|
||||||
.values(
|
|
||||||
"platform_name", "total_playtime"
|
|
||||||
) # Select the renamed field and total_playtime
|
|
||||||
.order_by("-total_playtime") # Optional: Order by the renamed platform name
|
|
||||||
)
|
)
|
||||||
for item in total_playtime_per_platform:
|
for item in total_playtime_per_platform:
|
||||||
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
|
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
|
||||||
@ -274,14 +312,20 @@ def stats(request, year: int = 0):
|
|||||||
"total_hours": format_duration(
|
"total_hours": format_duration(
|
||||||
year_sessions.total_duration_unformatted(), "%2.0H"
|
year_sessions.total_duration_unformatted(), "%2.0H"
|
||||||
),
|
),
|
||||||
"total_games": year_purchases.count(),
|
"total_games": year_played_purchases.count(),
|
||||||
"total_2023_games": year_purchases.filter(edition__year_released=year).count(),
|
"total_2023_games": year_played_purchases.filter(
|
||||||
"top_10_by_playtime_formatted": top_10_by_playtime,
|
edition__year_released=year
|
||||||
"top_10_by_playtime": top_10_by_playtime,
|
).count(),
|
||||||
|
"top_10_games_by_playtime": top_10_games_by_playtime,
|
||||||
"year": year,
|
"year": year,
|
||||||
"total_playtime_per_platform": total_playtime_per_platform,
|
"total_playtime_per_platform": total_playtime_per_platform,
|
||||||
|
"total_spent": total_spent,
|
||||||
|
"total_spent_currency": selected_currency,
|
||||||
|
"all_purchased_this_year": all_purchased_this_year,
|
||||||
|
"spent_per_game": int(total_spent / all_purchased_this_year.count()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request.session["return_path"] = request.path
|
||||||
return render(request, "stats.html", context)
|
return render(request, "stats.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "timetracker"
|
name = "timetracker"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
description = "A simple time tracker."
|
description = "A simple time tracker."
|
||||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
||||||
license = "GPL"
|
license = "GPL"
|
||||||
|
Reference in New Issue
Block a user