7 Commits

Author SHA1 Message Date
c44d8bf427 Improve time-related stuff
All checks were successful
continuous-integration/drone/push Build is passing
Add created_at to all models
Add modified_at to Session
Get rid of custom now() function
Make sure aware datetime is used everywhere
2023-11-15 19:14:09 +01:00
3f037b4c7c Only allow choosing purchases of selected edition
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-15 14:25:42 +01:00
8783d1fc8e Name and related_purchase validation for non-games 2023-11-15 13:04:47 +01:00
9a1d24dbfd Sort imports, remove cruft 2023-11-15 12:19:31 +01:00
4720660cff Fix wrong playrange ordering 2023-11-15 10:40:52 +01:00
e158bc0623 Improve how editions and purchases are displayed
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-15 10:37:24 +01:00
8982fc5086 Game View: order editions by year 2023-11-14 21:19:36 +01:00
16 changed files with 326 additions and 105 deletions

View File

@ -1,3 +1,9 @@
## Unreleased
## Improved
* game overview: improve how editions and purchases are displayed
* add purchase: only allow choosing purchases of selected edition
## 1.5.1 / 2023-11-14 21:10+01:00 ## 1.5.1 / 2023-11-14 21:10+01:00
## Improved ## Improved

View File

@ -72,6 +72,10 @@ textarea:disabled {
@apply dark:bg-slate-700 dark:text-slate-400; @apply dark:bg-slate-700 dark:text-slate-400;
} }
.errorlist {
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
form input, form input,
select, select,

View File

@ -1,12 +1,5 @@
import re import re
from datetime import datetime, timedelta from datetime import timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
def now() -> datetime:
return datetime.now(ZoneInfo(settings.TIME_ZONE))
def _safe_timedelta(duration: timedelta | int | None): def _safe_timedelta(duration: timedelta | int | None):

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from django.urls import reverse
from games.models import Game, Platform, Purchase, Session, Edition, Device from games.models import Game, Platform, Purchase, Session, Edition, Device
custom_date_widget = forms.DateInput(attrs={"type": "date"}) custom_date_widget = forms.DateInput(attrs={"type": "date"})
@ -50,6 +50,20 @@ class IncludePlatformSelect(forms.Select):
class PurchaseForm(forms.ModelForm): class PurchaseForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Automatically update related_purchase <select/>
# to only include purchases of the selected edition.
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
self.fields["edition"].widget.attrs.update(
{
"hx-get": related_purchase_by_edition_url,
"hx-target": "#id_related_purchase",
"hx-swap": "outerHTML",
}
)
edition = EditionChoiceField( edition = EditionChoiceField(
queryset=Edition.objects.order_by("sort_name"), queryset=Edition.objects.order_by("sort_name"),
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}), widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
@ -58,7 +72,8 @@ class PurchaseForm(forms.ModelForm):
related_purchase = forms.ModelChoiceField( related_purchase = forms.ModelChoiceField(
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by( queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
"edition__sort_name" "edition__sort_name"
) ),
required=False,
) )
class Meta: class Meta:
@ -82,6 +97,27 @@ class PurchaseForm(forms.ModelForm):
"name", "name",
] ]
def clean(self):
cleaned_data = super().clean()
purchase_type = cleaned_data.get("type")
related_purchase = cleaned_data.get("related_purchase")
name = cleaned_data.get("name")
# Set the type on the instance to use get_type_display()
# This is safe because we're not saving the instance.
self.instance.type = purchase_type
if purchase_type != Purchase.GAME:
type_display = self.instance.get_type_display()
if not related_purchase:
self.add_error(
"related_purchase",
f"{type_display} must have a related purchase.",
)
if not name:
self.add_error("name", f"{type_display} must have a name.")
return cleaned_data
class IncludeNameSelect(forms.Select): class IncludeNameSelect(forms.Select):
def create_option(self, name, value, *args, **kwargs): def create_option(self, name, value, *args, **kwargs):

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-11-14 21:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("games", "0028_purchase_name"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="related_purchase",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="related_purchases",
to="games.purchase",
),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-11-15 12:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0029_alter_purchase_related_purchase"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="name",
field=models.CharField(blank=True, default="", max_length=255, null=True),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 4.1.5 on 2023-11-15 13:51
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("games", "0030_alter_purchase_name"),
]
operations = [
migrations.AddField(
model_name="device",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="edition",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="game",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="platform",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="purchase",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="session",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
]

View File

@ -0,0 +1,52 @@
# Generated by Django 4.1.5 on 2023-11-15 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0031_device_created_at_edition_created_at_game_created_at_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="session",
options={"get_latest_by": "timestamp_start"},
),
migrations.AddField(
model_name="session",
name="modified_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AlterField(
model_name="device",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="edition",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="game",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="platform",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="purchase",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name="session",
name="created_at",
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -1,10 +1,8 @@
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo
from common.time import format_duration from common.time import format_duration
from django.conf import settings from datetime import timedelta
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.db.models import F, Manager, Sum from django.db.models import F, Manager, Sum
@ -13,6 +11,7 @@ class Game(models.Model):
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
year_released = models.IntegerField(null=True, blank=True, default=None) year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.name return self.name
@ -43,6 +42,7 @@ class Edition(models.Model):
) )
year_released = models.IntegerField(null=True, blank=True, default=None) year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.sort_name return self.sort_name
@ -120,12 +120,16 @@ class Purchase(models.Model):
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
) )
type = models.CharField(max_length=255, choices=TYPES, default=GAME) type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField( name = models.CharField(max_length=255, default="", null=True, blank=True)
max_length=255, default="Unknown Name", null=True, blank=True
)
related_purchase = models.ForeignKey( related_purchase = models.ForeignKey(
"Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True "Purchase",
on_delete=models.SET_NULL,
default=None,
null=True,
blank=True,
related_name="related_purchases",
) )
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
additional_info = [ additional_info = [
@ -144,12 +148,17 @@ class Purchase(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.type == Purchase.GAME: if self.type == Purchase.GAME:
self.name = "" self.name = ""
elif self.type != Purchase.GAME and not self.related_purchase:
raise ValidationError(
f"{self.get_type_display()} must have a related purchase."
)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Platform(models.Model): class Platform(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None) group = models.CharField(max_length=255, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.name return self.name
@ -167,6 +176,9 @@ class SessionQuerySet(models.QuerySet):
class Session(models.Model): class Session(models.Model):
class Meta:
get_latest_by = "timestamp_start"
purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE) purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE)
timestamp_start = models.DateTimeField() timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField(blank=True, null=True) timestamp_end = models.DateTimeField(blank=True, null=True)
@ -180,6 +192,8 @@ class Session(models.Model):
default=None, default=None,
) )
note = models.TextField(blank=True, null=True) note = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
objects = SessionQuerySet.as_manager() objects = SessionQuerySet.as_manager()
@ -188,10 +202,10 @@ class Session(models.Model):
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})" return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
def finish_now(self): def finish_now(self):
self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) self.timestamp_end = timezone.now()
def start_now(): def start_now():
self.timestamp_start = datetime.now(ZoneInfo(settings.TIME_ZONE)) self.timestamp_start = timezone.now()
def duration_seconds(self) -> timedelta: def duration_seconds(self) -> timedelta:
manual = timedelta(0) manual = timedelta(0)
@ -244,6 +258,7 @@ class Device(models.Model):
] ]
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN) type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"{self.name} ({self.get_type_display()})" return f"{self.name} ({self.get_type_display()})"

View File

@ -1231,6 +1231,19 @@ textarea:disabled) {
color: rgb(148 163 184 / var(--tw-text-opacity)); color: rgb(148 163 184 / var(--tw-text-opacity));
} }
.errorlist {
margin-top: 1rem;
margin-bottom: 0.25rem;
width: 300px;
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
padding-top: 0.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
--tw-text-opacity: 1;
color: rgb(226 232 240 / var(--tw-text-opacity));
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
form input, form input,
select, select,
@ -1429,6 +1442,10 @@ th label {
padding-right: 1rem; padding-right: 1rem;
} }
.sm\:pl-12 {
padding-left: 3rem;
}
.sm\:pl-2 { .sm\:pl-2 {
padding-left: 0.5rem; padding-left: 0.5rem;
} }

View File

@ -13,7 +13,7 @@
<link rel="stylesheet" href="{% static 'base.css' %}" /> <link rel="stylesheet" href="{% static 'base.css' %}" />
</head> </head>
<body class="dark"> <body class="dark" hx-indicator="#indicator">
<img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" /> <img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" />
<div class="dark:bg-gray-800 min-h-screen"> <div class="dark:bg-gray-800 min-h-screen">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded"> <nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">

View File

@ -12,7 +12,6 @@
id="last-session-start" id="last-session-start"
href="{% url 'start_session_same_as_last' last.id %}" href="{% url 'start_session_same_as_last' last.id %}"
hx-get="{% url 'start_session_same_as_last' last.id %}" hx-get="{% url 'start_session_same_as_last' last.id %}"
hx-indicator="#indicator"
hx-swap="afterbegin" hx-swap="afterbegin"
hx-target=".responsive-table tbody" hx-target=".responsive-table tbody"
hx-select=".responsive-table tbody tr:first-child" hx-select=".responsive-table tbody tr:first-child"

View File

@ -0,0 +1 @@
{{ form.related_purchase }}

View File

@ -13,11 +13,12 @@
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
</h1> </h1>
<h2 class="text-lg my-2 ml-2"> <h2 class="text-lg my-2 ml-2">
{{ total_hours }} <span class="dark:text-slate-500">total</span> {{ hours_sum }} <span class="dark:text-slate-500">total</span>
{{ session_average }} <span class="dark:text-slate-500">avg</span> {{ session_average }} <span class="dark:text-slate-500">avg</span>
({{ playrange }}) </h2> ({{ playrange }}) </h2>
<hr class="border-slate-500"> <hr class="border-slate-500">
<h1 class="text-3xl mt-4 mb-1">Editions <span class="dark:text-slate-500">({{ editions.count }})</span></h1> <h1 class="text-3xl mt-4 mb-1">Editions <span class="dark:text-slate-500">({{ edition_count }})</span> and Purchases <span class="dark:text-slate-500">({{ purchase_count }})</span></h1>
<ul> <ul>
{% for edition in editions %} {% for edition in editions %}
<li class="sm:pl-2 flex items-center"> <li class="sm:pl-2 flex items-center">
@ -32,32 +33,27 @@
{% url 'edit_edition' edition.id as edit_url %} {% url 'edit_edition' edition.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
</li> </li>
{% endfor %}
</ul>
<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 edition.game_purchases %}
<li class="sm:pl-2 flex items-center"> <li class="sm:pl-6 flex items-center">
{{ purchase.platform }} {{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}}
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
{% url 'edit_purchase' purchase.id as edit_url %} {% url 'edit_purchase' purchase.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
{% if purchase.related_purchases %} </li>
<li>
<ul> <ul>
{% for related_purchase in purchase.related_purchases %} {% for related_purchase in purchase.nongame_related_purchases %}
<li class="sm:pl-6 flex items-center"> <li class="sm:pl-12 flex items-center">
{{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}}) {{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}})
{% url 'edit_purchase' related_purchase.id as edit_url %} {% url 'edit_purchase' related_purchase.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %} {% include 'components/edit_button.html' with edit_url=edit_url %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</li>
{% endif %}
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endfor %}
</ul>
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center"> <h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
Sessions Sessions
<span class="dark:text-slate-500"> <span class="dark:text-slate-500">

View File

@ -44,6 +44,11 @@ urlpatterns = [
views.add_purchase, views.add_purchase,
name="add_purchase_for_edition", name="add_purchase_for_edition",
), ),
path(
"related-purchase-by-edition",
views.related_purchase_by_edition,
name="related_purchase_by_edition",
),
path("add-edition/", views.add_edition, name="add_edition"), path("add-edition/", views.add_edition, name="add_edition"),
path( path(
"add-edition-for-game/<int:game_id>", "add-edition-for-game/<int:game_id>",

View File

@ -1,12 +1,13 @@
from common.time import format_duration, now as now_with_tz from common.time import format_duration
from common.utils import safe_division from common.utils import safe_division
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, Count from django.db.models import Sum, F, Count, Prefetch
from django.db.models.functions import TruncDate from django.db.models.functions import TruncDate
from django.http import HttpRequest, HttpResponse, 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 django.utils import timezone
from typing import Callable, Any from typing import Callable, Any
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@ -32,19 +33,15 @@ def model_counts(request):
def stats_dropdown_year_range(request): def stats_dropdown_year_range(request):
result = { result = {"stats_dropdown_year_range": range(timezone.now().year, 1999, -1)}
"stats_dropdown_year_range": range(
datetime.now(ZoneInfo(settings.TIME_ZONE)).year, 1999, -1
)
}
return result return result
def add_session(request, purchase_id=None): def add_session(request, purchase_id=None):
context = {} context = {}
initial = {"timestamp_start": now_with_tz()} initial = {"timestamp_start": timezone.now()}
last = Session.objects.all().last() last = Session.objects.last()
if last != None: if last != None:
initial["purchase"] = last.purchase initial["purchase"] = last.purchase
@ -136,44 +133,52 @@ def edit_game(request, game_id=None):
def view_game(request, game_id=None): def view_game(request, game_id=None):
context = {}
game = Game.objects.get(id=game_id) game = Game.objects.get(id=game_id)
context["title"] = "View Game" nongame_related_purchases_prefetch = Prefetch(
context["game"] = game "related_purchases",
context["editions"] = Edition.objects.filter(game_id=game_id) queryset=Purchase.objects.exclude(type=Purchase.GAME),
game_purchases = ( to_attr="nongame_related_purchases",
Purchase.objects.filter(edition__game_id=game_id) )
.filter(type=Purchase.GAME) game_purchases_prefetch = Prefetch(
.order_by("date_purchased") "purchase_set",
queryset=Purchase.objects.filter(type=Purchase.GAME).prefetch_related(
nongame_related_purchases_prefetch
),
to_attr="game_purchases",
)
editions = (
Edition.objects.filter(game=game)
.prefetch_related(game_purchases_prefetch)
.order_by("year_released")
) )
for purchase in game_purchases:
purchase.related_purchases = Purchase.objects.exclude(
type=Purchase.GAME
).filter(related_purchase=purchase.id)
context["purchases"] = game_purchases sessions = Session.objects.filter(purchase__edition__game=game)
context["sessions"] = Session.objects.filter( session_count = sessions.count()
purchase__edition__game_id=game_id
).order_by("-timestamp_start")
context["total_hours"] = float(
format_duration(context["sessions"].total_duration_unformatted(), "%2.1H")
)
context["session_average"] = round(
(context["total_hours"]) / int(context["sessions"].count()), 1
)
# here first and last is flipped
# because sessions are ordered from newest to oldest
# so the most recent are on top
playrange_start = context["sessions"].last().timestamp_start.strftime("%b %Y")
playrange_end = context["sessions"].first().timestamp_start.strftime("%b %Y")
context["playrange"] = ( playrange_start = sessions.earliest().timestamp_start.strftime("%b %Y")
playrange_end = sessions.latest().timestamp_start.strftime("%b %Y")
playrange = (
playrange_start playrange_start
if playrange_start == playrange_end if playrange_start == playrange_end
else f"{playrange_start}{playrange_end}" else f"{playrange_start}{playrange_end}"
) )
total_hours = float(format_duration(sessions.total_duration_unformatted(), "%2.1H"))
context = {
"edition_count": editions.count(),
"editions": editions,
"game": game,
"playrange": playrange,
"purchase_count": Purchase.objects.filter(edition__game=game).count(),
"session_average": round(total_hours / int(session_count), 1),
"session_count": session_count,
"sessions_with_notes": sessions.exclude(note=""),
"sessions": sessions.order_by("-timestamp_start"),
"title": f"Game Overview - {game.name}",
"hours_sum": total_hours,
}
context["sessions_with_notes"] = context["sessions"].exclude(note="")
request.session["return_path"] = request.path request.session["return_path"] = request.path
return render(request, "view_game.html", context) return render(request, "view_game.html", context)
@ -204,17 +209,22 @@ def edit_edition(request, edition_id=None):
return render(request, "add.html", context) return render(request, "add.html", context)
def related_purchase_by_edition(request):
edition_id = request.GET.get("edition")
form = PurchaseForm()
form.fields["related_purchase"].queryset = Purchase.objects.filter(
edition_id=edition_id, type=Purchase.GAME
).order_by("edition__sort_name")
return render(request, "partials/related_purchase_field.html", {"form": form})
@use_custom_redirect @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).latest()
Session.objects.filter(purchase__edition__game_id=game_id)
.order_by("-timestamp_start")
.first()
)
session = SessionForm( session = SessionForm(
{ {
"purchase": last_session.purchase.id, "purchase": last_session.purchase.id,
"timestamp_start": now_with_tz(), "timestamp_start": timezone.now(),
"device": last_session.device, "device": last_session.device,
} }
) )
@ -227,7 +237,7 @@ def start_session_same_as_last(request, last_session_id: int):
session = SessionForm( session = SessionForm(
{ {
"purchase": last_session.purchase.id, "purchase": last_session.purchase.id,
"timestamp_start": now_with_tz(), "timestamp_start": timezone.now(),
"device": last_session.device, "device": last_session.device,
} }
) )
@ -277,19 +287,18 @@ def list_sessions(
context["title"] = "This year" context["title"] = "This year"
else: else:
# by default, sort from newest to oldest # by default, sort from newest to oldest
dataset = Session.objects.all().order_by("-timestamp_start") dataset = Session.objects.order_by("-timestamp_start")
for session in dataset: for session in dataset:
if session.timestamp_end == None and session.duration_manual == timedelta( if session.timestamp_end == None and session.duration_manual == timedelta(
seconds=0 seconds=0
): ):
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) session.timestamp_end = timezone.now()
session.unfinished = True session.unfinished = True
context["total_duration"] = dataset.total_duration_formatted() 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 context["last"] = Session.objects.latest()
context["last"] = Session.objects.all().order_by("timestamp_start").last()
return render(request, "list_sessions.html", context) return render(request, "list_sessions.html", context)
@ -299,7 +308,7 @@ def stats(request, year: int = 0):
if selected_year: if selected_year:
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year])) return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
if year == 0: if year == 0:
year = now_with_tz().year year = timezone.now().year
this_year_sessions = Session.objects.filter(timestamp_start__year=year) this_year_sessions = Session.objects.filter(timestamp_start__year=year)
selected_currency = "CZK" selected_currency = "CZK"
unique_days = ( unique_days = (
@ -432,7 +441,7 @@ def stats(request, year: int = 0):
def add_purchase(request, edition_id=None): def add_purchase(request, edition_id=None):
context = {} context = {}
initial = {"date_purchased": now_with_tz()} initial = {"date_purchased": timezone.now()}
if request.method == "POST": if request.method == "POST":
form = PurchaseForm(request.POST or None, initial=initial) form = PurchaseForm(request.POST or None, initial=initial)