10 Commits

Author SHA1 Message Date
5cc1652002 Always set game status change timestamp to now instead of game's last updated_at
All checks were successful
Django CI/CD / test (push) Successful in 1m16s
Django CI/CD / build-and-push (push) Successful in 2m15s
2025-06-07 20:28:14 +02:00
7cf2180192 Allow setting game to Finished when creating PlayEvent
All checks were successful
Django CI/CD / test (push) Successful in 1m17s
Django CI/CD / build-and-push (push) Successful in 2m21s
2025-06-07 20:14:14 +02:00
ad0641f95b Fix playtime stats per year
All checks were successful
Django CI/CD / test (push) Successful in 58s
Django CI/CD / build-and-push (push) Successful in 2m39s
2025-04-17 16:15:38 +02:00
abdcfdfe64 Redirect to previous page after editing a session
All checks were successful
Django CI/CD / test (push) Successful in 1m9s
Django CI/CD / build-and-push (push) Successful in 2m17s
2025-04-01 15:36:40 +02:00
31daf2efe0 Make game overview playthrough dropdown close when clicked outside
All checks were successful
Django CI/CD / test (push) Successful in 1m6s
Django CI/CD / build-and-push (push) Successful in 2m17s
2025-03-28 13:43:46 +01:00
6d53fca910 Always return timedelta in update_game_playtime
All checks were successful
Django CI/CD / test (push) Successful in 1m3s
Django CI/CD / build-and-push (push) Successful in 2m19s
2025-03-26 12:05:10 +01:00
f7e426e030 Make it easier to create a play event 2025-03-26 12:04:46 +01:00
b29e4edd72 Continue making use of improved duration handling
All checks were successful
Django CI/CD / test (push) Successful in 1m5s
Django CI/CD / build-and-push (push) Successful in 2m22s
2025-03-25 23:38:04 +01:00
3c58851b88 Improve form fields for duration and currency
All checks were successful
Django CI/CD / test (push) Successful in 1m0s
Django CI/CD / build-and-push (push) Successful in 2m10s
2025-03-25 22:46:17 +01:00
99f3540825 Improve duration handling for sessions and games 2025-03-25 22:46:01 +01:00
14 changed files with 231 additions and 108 deletions

View File

@ -13,7 +13,7 @@ durationformat_manual: str = "%H hours"
def _safe_timedelta(duration: timedelta | int | None):
if duration == None:
if duration is None:
return timedelta(0)
elif isinstance(duration, int):
return timedelta(seconds=duration)

View File

@ -1,4 +1,5 @@
from django import forms
from django.db import transaction
from django.urls import reverse
from common.utils import safe_getattr
@ -35,6 +36,13 @@ class SessionForm(forms.ModelForm):
widget=forms.Select(attrs={"autofocus": "autofocus"}),
)
duration_manual = forms.DurationField(
required=False,
widget=forms.TextInput(
attrs={"x-mask": "99:99:99", "placeholder": "HH:MM:SS", "x-data": ""}
),
label="Manual duration",
)
device = forms.ModelChoiceField(queryset=Device.objects.order_by("name"))
mark_as_played = forms.BooleanField(
@ -107,6 +115,18 @@ class PurchaseForm(forms.ModelForm):
required=False,
)
price_currency = forms.CharField(
widget=forms.TextInput(
attrs={
"x-mask": "aaa",
"placeholder": "CZK",
"x-data": "",
"class": "uppercase",
}
),
label="Currency",
)
class Meta:
widgets = {
"date_purchased": custom_date_widget,
@ -208,19 +228,30 @@ class PlayEventForm(forms.ModelForm):
widget=forms.Select(attrs={"autofocus": "autofocus"}),
)
mark_as_finished = forms.BooleanField(
required=False,
initial={"mark_as_finished": True},
label="Set game status to Finished",
)
class Meta:
model = PlayEvent
fields = [
"game",
"started",
"ended",
"note",
]
fields = ["game", "started", "ended", "note", "mark_as_finished"]
widgets = {
"started": custom_date_widget,
"ended": custom_date_widget,
}
def save(self, commit=True):
with transaction.atomic():
session = super().save(commit=False)
if self.cleaned_data.get("mark_as_finished"):
game_instance = session.game
game_instance.status = "f"
game_instance.save()
session.save()
return session
class GameStatusChangeForm(forms.ModelForm):
class Meta:

View File

@ -0,0 +1,32 @@
# Generated by Django 5.1.7 on 2025-03-25 20:30
import django.db.models.expressions
import django.db.models.functions.comparison
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0011_purchase_price_per_game"),
]
operations = [
migrations.RemoveField(
model_name="session",
name="duration_calculated",
),
migrations.AddField(
model_name="session",
name="duration_calculated",
field=models.GeneratedField(
db_persist=True,
expression=django.db.models.functions.comparison.Coalesce(
django.db.models.expressions.CombinedExpression(
models.F("timestamp_end"), "-", models.F("timestamp_start")
),
0,
),
output_field=models.DurationField(),
),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.1.7 on 2025-03-25 20:33
import datetime
from django.db import migrations, models
from django.db.models import F, Sum
def calculate_game_playtime(apps, schema_editor):
Game = apps.get_model("games", "Game")
games = Game.objects.all()
for game in games:
total_playtime = game.sessions.aggregate(
total_playtime=Sum(F("duration_total"))
)["total_playtime"]
if total_playtime:
game.playtime = total_playtime
game.save(update_fields=["playtime"])
class Migration(migrations.Migration):
dependencies = [
("games", "0012_alter_session_duration_calculated"),
]
operations = [
migrations.AddField(
model_name="game",
name="playtime",
field=models.DurationField(
blank=True, default=datetime.timedelta(0), editable=False
),
),
migrations.RunPython(calculate_game_playtime),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-03-25 20:46
import django.db.models.expressions
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0013_game_playtime'),
]
operations = [
migrations.AddField(
model_name='session',
name='duration_total',
field=models.GeneratedField(db_persist=True, expression=django.db.models.expressions.CombinedExpression(models.F('duration_calculated'), '+', models.F('duration_manual')), output_field=models.DurationField()),
),
]

View File

@ -29,6 +29,8 @@ class Game(models.Model):
"Platform", on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
)
playtime = models.DurationField(blank=True, editable=False, default=timedelta(0))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -78,6 +80,9 @@ class Game(models.Model):
def unplayed(self):
return self.status == self.Status.UNPLAYED
def playtime_formatted(self):
return format_duration(self.playtime, "%2.1H")
def save(self, *args, **kwargs):
if self.platform is None:
self.platform = get_sentinel_platform()
@ -153,13 +158,8 @@ class Purchase(models.Model):
platform = models.ForeignKey(
Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
)
date_purchased = models.DateField()
date_refunded = models.DateField(blank=True, null=True)
# move date_finished to PlayEvent model's Finished field
# also set Game's model Status field to Finished
# date_finished = models.DateField(blank=True, null=True)
# move date_dropped to Game model's field Status (Abandoned)
# date_dropped = models.DateField(blank=True, null=True)
date_purchased = models.DateField(verbose_name="Purchased")
date_refunded = models.DateField(blank=True, null=True, verbose_name="Refunded")
infinite = models.BooleanField(default=False)
price = models.FloatField(default=0)
price_currency = models.CharField(max_length=3, default="USD")
@ -288,10 +288,23 @@ class Session(models.Model):
default=None,
related_name="sessions",
)
timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField(blank=True, null=True)
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
duration_calculated = models.DurationField(blank=True, null=True)
timestamp_start = models.DateTimeField(verbose_name="Start")
timestamp_end = models.DateTimeField(blank=True, null=True, verbose_name="End")
duration_manual = models.DurationField(
blank=True, null=True, default=timedelta(0), verbose_name="Manual duration"
)
duration_calculated = GeneratedField(
expression=Coalesce(F("timestamp_end") - F("timestamp_start"), 0),
output_field=models.DurationField(),
db_persist=True,
editable=False,
)
duration_total = GeneratedField(
expression=F("duration_calculated") + F("duration_manual"),
output_field=models.DurationField(),
db_persist=True,
editable=False,
)
device = models.ForeignKey(
"Device",
on_delete=models.SET_DEFAULT,
@ -308,7 +321,7 @@ class Session(models.Model):
objects = SessionQuerySet.as_manager()
def __str__(self):
mark = ", manual" if self.is_manual() else ""
mark = "*" if self.is_manual() else ""
return f"{str(self.game)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
def finish_now(self):
@ -317,32 +330,18 @@ class Session(models.Model):
def start_now():
self.timestamp_start = timezone.now()
def duration_seconds(self) -> timedelta:
manual = timedelta(0)
calculated = timedelta(0)
if self.is_manual() and isinstance(self.duration_manual, timedelta):
manual = self.duration_manual
if self.timestamp_end is not None and self.timestamp_start is not None:
calculated = self.timestamp_end - self.timestamp_start
return timedelta(seconds=(manual + calculated).total_seconds())
def duration_formatted(self) -> str:
result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
result = format_duration(self.duration_total, "%02.1H")
return result
def duration_formatted_with_mark(self) -> str:
mark = "*" if self.is_manual() else ""
return f"{self.duration_formatted()}{mark}"
def is_manual(self) -> bool:
return not self.duration_manual == timedelta(0)
@property
def duration_sum(self) -> str:
return Session.objects.all().total_duration_formatted()
def save(self, *args, **kwargs) -> None:
if self.timestamp_start is not None and self.timestamp_end is not None:
self.duration_calculated = self.timestamp_end - self.timestamp_start
else:
self.duration_calculated = timedelta(0)
if not isinstance(self.duration_manual, timedelta):
self.duration_manual = timedelta(0)

View File

@ -1,8 +1,14 @@
from django.db.models.signals import m2m_changed, pre_save
import logging
from datetime import timedelta
from django.db.models import F, Sum
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_save
from django.dispatch import receiver
from django.utils.timezone import now
from games.models import Game, GameStatusChange, Purchase
from games.models import Game, GameStatusChange, Purchase, Session
logger = logging.getLogger("games")
@receiver(m2m_changed, sender=Purchase.games.through)
@ -12,6 +18,16 @@ def update_num_purchases(sender, instance, **kwargs):
instance.save(update_fields=["num_purchases"])
@receiver([post_save, post_delete], sender=Session)
def update_game_playtime(sender, instance, **kwargs):
game = instance.game
total_playtime = game.sessions.aggregate(
total_playtime=Sum(F("duration_calculated") + F("duration_manual"))
)["total_playtime"]
game.playtime = total_playtime if total_playtime else timedelta(0)
game.save(update_fields=["playtime"])
@receiver(pre_save, sender=Game)
def game_status_changed(sender, instance, **kwargs):
"""
@ -20,21 +36,23 @@ def game_status_changed(sender, instance, **kwargs):
try:
old_instance = sender.objects.get(pk=instance.pk)
old_status = old_instance.status
print("Got old instance")
logger.info("[game_status_changed]: Previous status exists.")
except sender.DoesNotExist:
# Handle the case where the instance was deleted before the signal was sent
print("Instance does not exist")
logger.info("[game_status_changed]: Previous status does not exist.")
return
if old_status != instance.status:
print("Status changed")
logger.info(
"[game_status_changed]: Status changed from {} to {}".format(
old_status, instance.status
)
)
GameStatusChange.objects.create(
game=instance,
old_status=old_status,
new_status=instance.status,
timestamp=instance.updated_at,
timestamp=now(),
)
else:
print("Status not changed")
print(f"{old_instance.status}")
print(f"{instance.status}")
logger.info("[game_status_changed]: Status has not changed")

View File

@ -1,5 +1,6 @@
<c-layouts.base>
{% load static %}
{% load duration_formatter %}
{% partialdef purchase-name %}
{% if purchase.type != 'game' %}
<c-gamelink :game_id=purchase.first_game.id>
@ -111,7 +112,7 @@
{% for month in month_playtimes %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">{{ month.month | date:"F" }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ month.playtime }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ month.playtime | format_duration }}</td>
</tr>
{% endfor %}
</tbody>
@ -157,7 +158,7 @@
<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>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime</th>
</tr>
</thead>
<tbody>
@ -166,7 +167,7 @@
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<c-gamelink :game_id=game.id :name=game.name />
</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ game.formatted_playtime }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ game.total_playtime | format_duration }}</td>
</tr>
{% endfor %}
</tbody>
@ -176,14 +177,14 @@
<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>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime</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>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ item.playtime | format_duration }}</td>
</tr>
{% endfor %}
</tbody>
@ -213,14 +214,14 @@
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
{% comment %} <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> {% endcomment %}
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in this_year_finished_this_year %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
{% comment %} <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> {% endcomment %}
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
@ -232,14 +233,14 @@
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
{% comment %} <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> {% endcomment %}
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in purchased_this_year_finished_this_year %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
{% comment %} <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> {% endcomment %}
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>

View File

@ -16,7 +16,7 @@
class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
{{ hours_sum }}
{{ game.playtime_formatted }}
</c-popover>
<c-popover id="popover-sessions" popover_content="Number of sessions" class="flex gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg"
@ -74,7 +74,7 @@
<span x-text="played"></span> times
</button>
</a>
<button type="button" x-on:click="open = !open" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border-e border-b border-t border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle">
<button type="button" x-on:click="open = !open" @click.outside="open = false" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border-e border-b border-t border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle">
<svg class="text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

View File

@ -0,0 +1,12 @@
from datetime import timedelta
from django import template
from common.time import durationformat, format_duration
register = template.Library()
@register.filter(name="format_duration")
def filter_format_duration(duration: timedelta, argument: str = durationformat):
return format_duration(duration, format_string=argument)

View File

@ -22,8 +22,6 @@ from common.components import (
)
from common.time import (
dateformat,
durationformat,
durationformat_manual,
format_duration,
local_strftime,
timeformat,
@ -310,11 +308,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
session_id=session.pk,
),
f"{local_strftime(session.timestamp_start)}{f'{local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
(
format_duration(session.duration_calculated, durationformat)
if session.duration_calculated
else f"{format_duration(session.duration_manual, durationformat_manual)}*"
),
session.duration_formatted_with_mark,
render_to_string(
"cotton/button_group.html",
{

View File

@ -22,10 +22,10 @@ def model_counts(request: HttpRequest) -> dict[str, bool]:
timestamp_start__day=this_day,
timestamp_start__month=this_month,
timestamp_start__year=this_year,
).aggregate(time=Sum(F("duration_calculated") + F("duration_manual")))["time"]
).aggregate(time=Sum(F("duration_total")))["time"]
last_7_played = Session.objects.filter(
timestamp_start__gte=(now - timedelta(days=7))
).aggregate(time=Sum(F("duration_calculated") + F("duration_manual")))["time"]
).aggregate(time=Sum(F("duration_total")))["time"]
return {
"game_available": Game.objects.exists(),
@ -137,19 +137,13 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
)
total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = (
Game.objects.filter(sessions__in=this_year_sessions)
.annotate(
total_playtime=Sum(
F("sessions__duration_calculated") + F("sessions__duration_manual")
)
)
.values("id", "name", "total_playtime")
)
games_with_playtime = Game.objects.filter(
sessions__in=this_year_sessions
).distinct()
month_playtimes = (
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
.values("month")
.annotate(playtime=Sum("duration_calculated"))
.annotate(playtime=Sum("duration_total"))
.order_by("month")
)
for month in month_playtimes:
@ -162,18 +156,14 @@ def stats_alltime(request: HttpRequest) -> HttpResponse:
.first()
)
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 = (
this_year_sessions.values("game__platform__name")
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
.annotate(playtime=Sum(F("duration_total")))
.annotate(platform_name=F("game__platform__name"))
.values("platform_name", "total_playtime")
.order_by("-total_playtime")
.values("platform_name", "playtime")
.order_by("-playtime")
)
for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
backlog_decrease_count = (
Purchase.objects.all().intersection(purchases_finished_this_year).count()
@ -362,7 +352,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
)
)
purchased_this_year_finished_this_year = (
this_year_purchases_without_refunded.filter(games__playevents__ended__year=year)
this_year_purchases_without_refunded.filter(
games__playevents__ended__year=year
).annotate(
game_name=F("games__name"), date_finished=F("games__playevents__ended")
)
).order_by("games__playevents__ended")
this_year_spendings = this_year_purchases_without_refunded.aggregate(
@ -371,22 +365,21 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
total_spent = this_year_spendings["total_spent"] or 0
games_with_playtime = (
Game.objects.filter(sessions__in=this_year_sessions)
Game.objects.filter(sessions__timestamp_start__year=year)
.annotate(
total_playtime=Sum(
F("sessions__duration_calculated") + F("sessions__duration_manual")
F("sessions__duration_calculated"),
)
)
.values("id", "name", "total_playtime")
.filter(total_playtime__gt=timedelta(0))
)
month_playtimes = (
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
.values("month")
.annotate(playtime=Sum("duration_calculated"))
.annotate(playtime=Sum("duration_total"))
.order_by("month")
)
for month in month_playtimes:
month["playtime"] = format_duration(month["playtime"], "%2.0H")
highest_session_average_game = (
Game.objects.filter(sessions__in=this_year_sessions)
@ -395,18 +388,14 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
.first()
)
top_10_games_by_playtime = games_with_playtime.order_by("-total_playtime")
for game in top_10_games_by_playtime:
game["formatted_playtime"] = format_duration(game["total_playtime"], "%2.0H")
total_playtime_per_platform = (
this_year_sessions.values("game__platform__name")
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
.annotate(playtime=Sum(F("duration_total")))
.annotate(platform_name=F("game__platform__name"))
.values("platform_name", "total_playtime")
.order_by("-total_playtime")
.values("platform_name", "playtime")
.order_by("-playtime")
)
for item in total_playtime_per_platform:
item["formatted_playtime"] = format_duration(item["total_playtime"], "%2.0H")
backlog_decrease_count = (
Purchase.objects.filter(date_purchased__year__lt=year)

View File

@ -115,6 +115,8 @@ def add_playevent(request: HttpRequest, game_id: int = 0) -> HttpResponse:
# coming from add_playevent_for_game url path
game = get_object_or_404(Game, id=game_id)
initial["game"] = game
initial["started"] = game.sessions.earliest().timestamp_start
initial["ended"] = game.sessions.latest().timestamp_start
form = PlayEventForm(request.POST or None, initial=initial)
if form.is_valid():
form.save()

View File

@ -20,16 +20,12 @@ from common.components import (
)
from common.time import (
dateformat,
durationformat,
durationformat_manual,
format_duration,
local_strftime,
timeformat,
)
from common.utils import truncate
from games.forms import SessionForm
from games.models import Game, Session
from games.views.general import use_custom_redirect
@login_required
@ -130,11 +126,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
[
NameWithIcon(session_id=session.pk),
f"{local_strftime(session.timestamp_start)}{f'{local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
(
format_duration(session.duration_calculated, durationformat)
if session.duration_calculated
else f"{format_duration(session.duration_manual, durationformat_manual)}*"
),
session.duration_formatted_with_mark,
session.device,
session.created_at.strftime(dateformat),
render_to_string(
@ -222,7 +214,6 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
@login_required
@use_custom_redirect
def edit_session(request: HttpRequest, session_id: int) -> HttpResponse:
context = {}
session = get_object_or_404(Session, id=session_id)