From c44d8bf427ef32c8be401f709094b5cb6974fc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Wed, 15 Nov 2023 17:47:51 +0100 Subject: [PATCH] Improve time-related stuff Add created_at to all models Add modified_at to Session Get rid of custom now() function Make sure aware datetime is used everywhere --- common/time.py | 9 +--- ...ion_created_at_game_created_at_and_more.py | 44 ++++++++++++++++ ...on_options_session_modified_at_and_more.py | 52 +++++++++++++++++++ games/models.py | 19 +++++-- games/views.py | 42 ++++++--------- 5 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 games/migrations/0031_device_created_at_edition_created_at_game_created_at_and_more.py create mode 100644 games/migrations/0032_alter_session_options_session_modified_at_and_more.py diff --git a/common/time.py b/common/time.py index be09c1f..a51f2ce 100644 --- a/common/time.py +++ b/common/time.py @@ -1,12 +1,5 @@ import re -from datetime import datetime, timedelta -from zoneinfo import ZoneInfo - -from django.conf import settings - - -def now() -> datetime: - return datetime.now(ZoneInfo(settings.TIME_ZONE)) +from datetime import timedelta def _safe_timedelta(duration: timedelta | int | None): diff --git a/games/migrations/0031_device_created_at_edition_created_at_game_created_at_and_more.py b/games/migrations/0031_device_created_at_edition_created_at_game_created_at_and_more.py new file mode 100644 index 0000000..f417e17 --- /dev/null +++ b/games/migrations/0031_device_created_at_edition_created_at_game_created_at_and_more.py @@ -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), + ), + ] diff --git a/games/migrations/0032_alter_session_options_session_modified_at_and_more.py b/games/migrations/0032_alter_session_options_session_modified_at_and_more.py new file mode 100644 index 0000000..cd94d0a --- /dev/null +++ b/games/migrations/0032_alter_session_options_session_modified_at_and_more.py @@ -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), + ), + ] diff --git a/games/models.py b/games/models.py index 7ac16d8..02412de 100644 --- a/games/models.py +++ b/games/models.py @@ -1,10 +1,9 @@ from common.time import format_duration -from datetime import datetime, timedelta -from django.conf import settings +from datetime import timedelta 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 zoneinfo import ZoneInfo class Game(models.Model): @@ -12,6 +11,7 @@ class Game(models.Model): sort_name = models.CharField(max_length=255, 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) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name @@ -42,6 +42,7 @@ class Edition(models.Model): ) year_released = models.IntegerField(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): return self.sort_name @@ -128,6 +129,7 @@ class Purchase(models.Model): blank=True, related_name="related_purchases", ) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): additional_info = [ @@ -156,6 +158,7 @@ class Purchase(models.Model): class Platform(models.Model): name = models.CharField(max_length=255) group = models.CharField(max_length=255, null=True, blank=True, default=None) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name @@ -173,6 +176,9 @@ class SessionQuerySet(models.QuerySet): class Session(models.Model): + class Meta: + get_latest_by = "timestamp_start" + purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE) timestamp_start = models.DateTimeField() timestamp_end = models.DateTimeField(blank=True, null=True) @@ -186,6 +192,8 @@ class Session(models.Model): default=None, ) 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() @@ -194,10 +202,10 @@ class Session(models.Model): return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})" def finish_now(self): - self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) + self.timestamp_end = timezone.now() def start_now(): - self.timestamp_start = datetime.now(ZoneInfo(settings.TIME_ZONE)) + self.timestamp_start = timezone.now() def duration_seconds(self) -> timedelta: manual = timedelta(0) @@ -250,6 +258,7 @@ class Device(models.Model): ] name = models.CharField(max_length=255) type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN) + created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} ({self.get_type_display()})" diff --git a/games/views.py b/games/views.py index b4149c3..fa568fb 100644 --- a/games/views.py +++ b/games/views.py @@ -1,4 +1,4 @@ -from common.time import format_duration, now as now_with_tz +from common.time import format_duration from common.utils import safe_division from datetime import datetime, timedelta from django.conf import settings @@ -7,6 +7,7 @@ from django.db.models.functions import TruncDate from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse +from django.utils import timezone from typing import Callable, Any from zoneinfo import ZoneInfo @@ -32,19 +33,15 @@ def model_counts(request): def stats_dropdown_year_range(request): - result = { - "stats_dropdown_year_range": range( - datetime.now(ZoneInfo(settings.TIME_ZONE)).year, 1999, -1 - ) - } + result = {"stats_dropdown_year_range": range(timezone.now().year, 1999, -1)} return result def add_session(request, purchase_id=None): context = {} - initial = {"timestamp_start": now_with_tz()} + initial = {"timestamp_start": timezone.now()} - last = Session.objects.all().last() + last = Session.objects.last() if last != None: initial["purchase"] = last.purchase @@ -155,13 +152,11 @@ def view_game(request, game_id=None): .order_by("year_released") ) - sessions = Session.objects.filter(purchase__edition__game=game).order_by( - "timestamp_start" - ) + sessions = Session.objects.filter(purchase__edition__game=game) session_count = sessions.count() - playrange_start = sessions.first().timestamp_start.strftime("%b %Y") - playrange_end = sessions.last().timestamp_start.strftime("%b %Y") + playrange_start = sessions.earliest().timestamp_start.strftime("%b %Y") + playrange_end = sessions.latest().timestamp_start.strftime("%b %Y") playrange = ( playrange_start @@ -225,15 +220,11 @@ def related_purchase_by_edition(request): @use_custom_redirect def start_game_session(request, game_id: int): - last_session = ( - Session.objects.filter(purchase__edition__game_id=game_id) - .order_by("-timestamp_start") - .first() - ) + last_session = Session.objects.filter(purchase__edition__game_id=game_id).latest() session = SessionForm( { "purchase": last_session.purchase.id, - "timestamp_start": now_with_tz(), + "timestamp_start": timezone.now(), "device": last_session.device, } ) @@ -246,7 +237,7 @@ def start_session_same_as_last(request, last_session_id: int): session = SessionForm( { "purchase": last_session.purchase.id, - "timestamp_start": now_with_tz(), + "timestamp_start": timezone.now(), "device": last_session.device, } ) @@ -296,19 +287,18 @@ def list_sessions( context["title"] = "This year" else: # 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: if session.timestamp_end == None and session.duration_manual == timedelta( seconds=0 ): - session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) + session.timestamp_end = timezone.now() session.unfinished = True context["total_duration"] = dataset.total_duration_formatted() context["dataset"] = dataset - # 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.latest() return render(request, "list_sessions.html", context) @@ -318,7 +308,7 @@ def stats(request, year: int = 0): if selected_year: return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year])) if year == 0: - year = now_with_tz().year + year = timezone.now().year this_year_sessions = Session.objects.filter(timestamp_start__year=year) selected_currency = "CZK" unique_days = ( @@ -451,7 +441,7 @@ def stats(request, year: int = 0): def add_purchase(request, edition_id=None): context = {} - initial = {"date_purchased": now_with_tz()} + initial = {"date_purchased": timezone.now()} if request.method == "POST": form = PurchaseForm(request.POST or None, initial=initial)