Improve game overview

- add counts for each section
- add average hours per session
This commit is contained in:
Lukáš Kucharczyk 2023-10-09 00:00:45 +02:00
parent b9869cf232
commit caa2ae06e1
6 changed files with 70 additions and 173 deletions

View File

@ -36,9 +36,9 @@ def format_duration(
minute_seconds = 60 minute_seconds = 60
hour_seconds = 60 * minute_seconds hour_seconds = 60 * minute_seconds
day_seconds = 24 * hour_seconds day_seconds = 24 * hour_seconds
duration = _safe_timedelta(duration) safe_duration = _safe_timedelta(duration)
# we don't need float # we don't need float
seconds_total = int(duration.total_seconds()) seconds_total = int(safe_duration.total_seconds())
# timestamps where end is before start # timestamps where end is before start
if seconds_total < 0: if seconds_total < 0:
seconds_total = 0 seconds_total = 0

View File

@ -73,11 +73,14 @@ class Platform(models.Model):
class SessionQuerySet(models.QuerySet): class SessionQuerySet(models.QuerySet):
def total_duration(self): def total_duration_formatted(self):
return format_duration(self.total_duration_unformatted())
def total_duration_unformatted(self):
result = self.aggregate( result = self.aggregate(
duration=Sum(F("duration_calculated") + F("duration_manual")) duration=Sum(F("duration_calculated") + F("duration_manual"))
) )
return format_duration(result["duration"]) return result["duration"]
class Session(models.Model): class Session(models.Model):
@ -116,7 +119,7 @@ class Session(models.Model):
@property @property
def duration_sum(self) -> str: def duration_sum(self) -> str:
return Session.objects.all().total_duration() return Session.objects.all().total_duration_formatted()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.timestamp_start != None and self.timestamp_end != None: if self.timestamp_start != None and self.timestamp_end != None:

View File

@ -771,24 +771,14 @@ select {
top: 0.75rem; top: 0.75rem;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mx-2 { .mx-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.my-1 { .mx-auto {
margin-top: 0.25rem; margin-left: auto;
margin-bottom: 0.25rem; margin-right: auto;
} }
.my-2 { .my-2 {
@ -796,10 +786,23 @@ select {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-4 { .mb-4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.ml-2 {
margin-left: 0.5rem;
}
.mr-4 { .mr-4 {
margin-right: 1rem; margin-right: 1rem;
} }
@ -808,30 +811,6 @@ select {
margin-top: 1rem; margin-top: 1rem;
} }
.mt-1 {
margin-top: 0.25rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.ml-1 {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -848,10 +827,6 @@ select {
display: table; display: table;
} }
.list-item {
display: list-item;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -872,30 +847,18 @@ select {
width: 100%; width: 100%;
} }
.w-5 {
width: 1.25rem;
}
.max-w-screen-lg { .max-w-screen-lg {
max-width: 1024px; max-width: 1024px;
} }
.max-w-xs {
max-width: 20rem;
}
.max-w-lg {
max-width: 32rem;
}
.max-w-3xl {
max-width: 48rem;
}
.max-w-sm { .max-w-sm {
max-width: 24rem; max-width: 24rem;
} }
.max-w-xs {
max-width: 20rem;
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@ -906,10 +869,6 @@ select {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.list-disc {
list-style-type: disc;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@ -953,16 +912,6 @@ select {
border-color: rgb(229 231 235 / var(--tw-border-opacity)); border-color: rgb(229 231 235 / var(--tw-border-opacity));
} }
.border-red-300 {
--tw-border-opacity: 1;
border-color: rgb(252 165 165 / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.border-slate-500 { .border-slate-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity)); border-color: rgb(100 116 139 / var(--tw-border-opacity));
@ -978,11 +927,6 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-slate-400 {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
}
.p-4 { .p-4 {
padding: 1rem; padding: 1rem;
} }
@ -1018,12 +962,9 @@ select {
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
.font-serif { .text-3xl {
font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; font-size: 1.875rem;
} line-height: 2.25rem;
.font-sans {
font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
.text-4xl { .text-4xl {
@ -1036,6 +977,11 @@ select {
line-height: 1.5rem; line-height: 1.5rem;
} }
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl { .text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -1046,62 +992,10 @@ select {
line-height: 1rem; line-height: 1rem;
} }
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-9xl {
font-size: 8rem;
line-height: 1;
}
.text-8xl {
font-size: 6rem;
line-height: 1;
}
.text-7xl {
font-size: 4.5rem;
line-height: 1;
}
.text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
.font-bold {
font-weight: 700;
}
.capitalize {
text-transform: capitalize;
}
.italic {
font-style: italic;
}
.text-slate-300 { .text-slate-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
@ -1117,23 +1011,10 @@ select {
color: rgb(253 224 71 / var(--tw-text-opacity)); color: rgb(253 224 71 / var(--tw-text-opacity));
} }
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.underline { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }
.decoration-red-500 {
text-decoration-color: #ef4444;
}
.decoration-slate-400 {
text-decoration-color: #94a3b8;
}
.decoration-slate-500 { .decoration-slate-500 {
text-decoration-color: #64748b; text-decoration-color: #64748b;
} }
@ -1403,11 +1284,17 @@ th label {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
@media (min-width: 640px) { :is(.dark .dark\:text-slate-400) {
.sm\:ml-2 { --tw-text-opacity: 1;
margin-left: 0.5rem; color: rgb(148 163 184 / var(--tw-text-opacity));
} }
:is(.dark .dark\:text-slate-500) {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
@media (min-width: 640px) {
.sm\:inline { .sm\:inline {
display: inline; display: inline;
} }
@ -1420,10 +1307,6 @@ th label {
max-width: 28rem; max-width: 28rem;
} }
.sm\:max-w-lg {
max-width: 32rem;
}
.sm\:max-w-xl { .sm\:max-w-xl {
max-width: 36rem; max-width: 36rem;
} }
@ -1475,11 +1358,11 @@ th label {
display: table-cell; display: table-cell;
} }
.lg\:max-w-lg {
max-width: 32rem;
}
.lg\:max-w-3xl { .lg\:max-w-3xl {
max-width: 48rem; max-width: 48rem;
} }
.lg\:max-w-lg {
max-width: 32rem;
}
} }

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="text-slate-300 mx-auto max-w-screen-lg text-center"> <div class="text-slate-300 mx-auto max-w-screen-lg text-center">
{% if session_count > 0 %} {% if session_count > 0 %}
You have played a total of {{ session_count }} sessions for a total of {{ total_duration }}. You have played a total of {{ session_count }} sessions for a total of {{ total_duration_formatted }}.
{% elif not game_available or not platform_available %} {% elif not game_available or not platform_available %}
There are no games in the database. Start by clicking "New Game" and "New Platform". There are no games in the database. Start by clicking "New Game" and "New Platform".
{% elif not purchase_available %} {% elif not purchase_available %}
@ -14,4 +14,4 @@
You haven't played any games yet. Click "New Session" to add one now. You haven't played any games yet. Click "New Session" to add one now.
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -6,10 +6,15 @@
{% block content %} {% block content %}
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<h1 class="text-4xl">{{ game.name }} (#{{ game.pk }})</h1> <h1 class="text-4xl">{{ game.name }} <span class="dark:text-slate-500">(#{{ game.pk }})</span></h1>
<h2 class="text-lg my-2 ml-2">{{ total_playtime }} ({{ first_session.timestamp_start | date:"M Y"}} — {{ last_session.timestamp_start | date:"M Y"}}) </h2> <h2 class="text-lg my-2 ml-2">
{{ total_hours }} <span class="dark:text-slate-500">total</span>
{{ session_average }} <span class="dark:text-slate-500">avg</span>
({{ first_session.timestamp_start | date:"M Y"}}
{{ last_session.timestamp_start | date:"M Y"}}) </h2>
<hr class="border-slate-500"> <hr class="border-slate-500">
<h1 class="text-3xl mt-4 mb-1">Editions</h1> <h1 class="text-3xl mt-4 mb-1">Editions <span class="dark:text-slate-500">({{ editions.count }})</span></h1>
<ul> <ul>
{% for edition in editions %} {% for edition in editions %}
<li class="sm:pl-2"> <li class="sm:pl-2">
@ -24,13 +29,13 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<h1 class="text-3xl mt-4 mb-1">Purchases</h1> <h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
<ul> <ul>
{% for purchase in purchases %} {% for purchase in purchases %}
<li class="sm:pl-2">{{ purchase.platform }} ({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})</li> <li class="sm:pl-2">{{ purchase.platform }} ({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})</li>
{% endfor %} {% endfor %}
</ul> </ul>
<h1 class="text-3xl mt-4 mb-1">Sessions</h1> <h1 class="text-3xl mt-4 mb-1">Sessions <span class="dark:text-slate-500">({{ sessions.count }})</span></h1>
<ul> <ul>
{% for session in sessions %} {% for session in sessions %}
<li class="sm:pl-2">{{ session.timestamp_start | date:"d/m/Y" }} ({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})</li> <li class="sm:pl-2">{{ session.timestamp_start | date:"d/m/Y" }} ({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})</li>

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from common.time import now as now_with_tz from common.time import now as now_with_tz
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
@ -100,7 +101,12 @@ def view_game(request, game_id=None):
context["sessions"] = Session.objects.filter( context["sessions"] = Session.objects.filter(
purchase__edition__game_id=game_id purchase__edition__game_id=game_id
).order_by("-timestamp_start") ).order_by("-timestamp_start")
context["total_playtime"] = context["sessions"].total_duration() context["total_hours"] = int(
format_duration(context["sessions"].total_duration_unformatted(), "%H")
)
context["session_average"] = round(
(context["total_hours"]) / int(context["sessions"].count()), 1
)
# here first and last is flipped # here first and last is flipped
# because sessions are ordered from newest to oldest # because sessions are ordered from newest to oldest
# so the most recent are on top # so the most recent are on top
@ -197,7 +203,7 @@ def list_sessions(
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
session.unfinished = True session.unfinished = True
context["total_duration"] = dataset.total_duration() context["total_duration"] = dataset.total_duration_formatted()
context["dataset"] = dataset context["dataset"] = dataset
# cannot use dataset[0] here because that might be only partial QuerySet # cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.all().order_by("timestamp_start").last() context["last"] = Session.objects.all().order_by("timestamp_start").last()