parent
a5b2854bf6
commit
57d4fd7212
|
@ -1,5 +1,8 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### New
|
||||||
|
* Add yearly stats page (https://git.kucharczyk.xyz/lukas/timetracker/issues/15)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
* Add a button to start session from game overview
|
* Add a button to start session from game overview
|
||||||
|
|
||||||
|
|
|
@ -791,6 +791,16 @@ select {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.my-5 {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-1 {
|
.mb-1 {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -889,10 +899,6 @@ select {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-row {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -905,10 +911,6 @@ select {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-baseline {
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-center {
|
.justify-center {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
@ -1017,6 +1019,11 @@ select {
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-5xl {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.text-base {
|
.text-base {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
|
@ -1037,11 +1044,6 @@ select {
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-sm {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
|
||||||
|
<h1 class="text-5xl text-center my-6">Stats for {{ year }}</h1>
|
||||||
|
<table class="responsive-table">
|
||||||
|
<thead>
|
||||||
|
<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">Total games</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Total 2023 games</th>
|
||||||
|
</tr>
|
||||||
|
<tbody>
|
||||||
|
<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_games }}</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h1 class="text-5xl text-center my-6">Top games by playtime</h1>
|
||||||
|
<table class="responsive-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for purchase in top_10_by_playtime %}
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.formatted_playtime }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h1 class="text-5xl text-center my-6">Platforms by playtime</h1>
|
||||||
|
<table class="responsive-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Platform</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime (hours)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in total_playtime_per_platform %}
|
||||||
|
<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.formatted_playtime }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
|
@ -73,4 +73,9 @@ urlpatterns = [
|
||||||
{"filter": "ownership_type"},
|
{"filter": "ownership_type"},
|
||||||
name="list_sessions_by_ownership_type",
|
name="list_sessions_by_ownership_type",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"stats/<int:year>",
|
||||||
|
views.stats,
|
||||||
|
name="stats_by_year",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,6 +5,7 @@ from common.time import now as now_with_tz
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
from django.db.models import Sum, F
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
GameForm,
|
GameForm,
|
||||||
|
@ -229,6 +230,48 @@ def list_sessions(
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def stats(request, year: int):
|
||||||
|
first_day_of_year = datetime(year, 1, 1)
|
||||||
|
year_sessions = Session.objects.filter(timestamp_start__gte=first_day_of_year)
|
||||||
|
year_purchases = Purchase.objects.filter(session__in=year_sessions).distinct()
|
||||||
|
year_purchases_with_playtime = year_purchases.annotate(
|
||||||
|
total_playtime=Sum(
|
||||||
|
F("session__duration_calculated") + F("session__duration_manual")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
top_10_by_playtime = year_purchases_with_playtime.order_by("-total_playtime")[:10]
|
||||||
|
for purchase in top_10_by_playtime:
|
||||||
|
purchase.formatted_playtime = format_duration(purchase.total_playtime, "%2.0H")
|
||||||
|
|
||||||
|
total_playtime_per_platform = (
|
||||||
|
year_sessions.values("purchase__platform__name") # Group by platform name
|
||||||
|
.annotate(
|
||||||
|
total_playtime=Sum(F("duration_calculated") + F("duration_manual"))
|
||||||
|
) # Sum the duration_calculated for each group
|
||||||
|
.annotate(platform_name=F("purchase__platform__name")) # Rename the field
|
||||||
|
.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:
|
||||||
|
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"total_hours": format_duration(
|
||||||
|
year_sessions.total_duration_unformatted(), "%2.0H"
|
||||||
|
),
|
||||||
|
"total_games": year_purchases.count(),
|
||||||
|
"total_2023_games": year_purchases.filter(edition__year_released=year).count(),
|
||||||
|
"top_10_by_playtime_formatted": top_10_by_playtime,
|
||||||
|
"top_10_by_playtime": top_10_by_playtime,
|
||||||
|
"year": year,
|
||||||
|
"total_playtime_per_platform": total_playtime_per_platform,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, "stats.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_purchase(request):
|
def add_purchase(request):
|
||||||
context = {}
|
context = {}
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
Loading…
Reference in New Issue