Compare commits
8 Commits
3c58851b88
...
main
Author | SHA1 | Date | |
---|---|---|---|
5cc1652002
|
|||
7cf2180192
|
|||
ad0641f95b
|
|||
abdcfdfe64
|
|||
31daf2efe0
|
|||
6d53fca910
|
|||
f7e426e030
|
|||
b29e4edd72
|
@ -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)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
|
||||
from common.utils import safe_getattr
|
||||
@ -227,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:
|
||||
|
@ -1,3 +1,6 @@
|
||||
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
|
||||
@ -5,6 +8,8 @@ from django.utils.timezone import now
|
||||
|
||||
from games.models import Game, GameStatusChange, Purchase, Session
|
||||
|
||||
logger = logging.getLogger("games")
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Purchase.games.through)
|
||||
def update_num_purchases(sender, instance, **kwargs):
|
||||
@ -19,7 +24,7 @@ def update_game_playtime(sender, instance, **kwargs):
|
||||
total_playtime = game.sessions.aggregate(
|
||||
total_playtime=Sum(F("duration_calculated") + F("duration_manual"))
|
||||
)["total_playtime"]
|
||||
game.playtime = total_playtime if total_playtime else 0
|
||||
game.playtime = total_playtime if total_playtime else timedelta(0)
|
||||
game.save(update_fields=["playtime"])
|
||||
|
||||
|
||||
@ -31,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")
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
12
games/templatetags/duration_formatter.py
Normal file
12
games/templatetags/duration_formatter.py
Normal 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)
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -26,7 +26,6 @@ from common.time import (
|
||||
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
|
||||
@ -215,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)
|
||||
|
Reference in New Issue
Block a user