1 Commits

Author SHA1 Message Date
a30c54ef44 initial commit
All checks were successful
Django CI/CD / test (push) Successful in 1m4s
Django CI/CD / build-and-push (push) Has been skipped
2023-12-22 12:42:40 +01:00
7 changed files with 114 additions and 142 deletions

View File

@ -83,7 +83,6 @@ class PurchaseForm(forms.ModelForm):
"date_purchased": custom_date_widget, "date_purchased": custom_date_widget,
"date_refunded": custom_date_widget, "date_refunded": custom_date_widget,
"date_finished": custom_date_widget, "date_finished": custom_date_widget,
"date_dropped": custom_date_widget,
} }
model = Purchase model = Purchase
fields = [ fields = [
@ -92,8 +91,7 @@ class PurchaseForm(forms.ModelForm):
"date_purchased", "date_purchased",
"date_refunded", "date_refunded",
"date_finished", "date_finished",
"date_dropped", "status",
"infinite",
"price", "price",
"price_currency", "price_currency",
"ownership_type", "ownership_type",

View File

@ -1,23 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-03 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0033_alter_edition_unique_together"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="date_dropped",
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name="purchase",
name="infinite",
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2023-12-22 10:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0033_alter_edition_unique_together"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="status",
field=models.IntegerField(
choices=[
(0, "Unplayed"),
(1, "Playing"),
(2, "Dropped"),
(3, "Finished"),
],
default=0,
),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.7 on 2023-12-22 10:09
from django.db import migrations
from games.models import Purchase
def set_default_state(apps, schema_editor):
Purchase.objects.filter(session__isnull=False).update(
status=Purchase.PurchaseState.PLAYING
)
Purchase.objects.filter(date_finished__isnull=False).update(
status=Purchase.PurchaseState.FINISHED
)
class Migration(migrations.Migration):
dependencies = [
("games", "0034_purchase_status"),
]
operations = [migrations.RunPython(set_default_state)]

View File

@ -116,8 +116,6 @@ class Purchase(models.Model):
date_purchased = models.DateField() date_purchased = models.DateField()
date_refunded = models.DateField(blank=True, null=True) date_refunded = models.DateField(blank=True, null=True)
date_finished = models.DateField(blank=True, null=True) date_finished = models.DateField(blank=True, null=True)
date_dropped = models.DateField(blank=True, null=True)
infinite = models.BooleanField(default=False)
price = models.IntegerField(default=0) price = models.IntegerField(default=0)
price_currency = models.CharField(max_length=3, default="USD") price_currency = models.CharField(max_length=3, default="USD")
ownership_type = models.CharField( ownership_type = models.CharField(
@ -135,6 +133,25 @@ class Purchase(models.Model):
) )
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
class PurchaseState(models.IntegerChoices):
UNPLAYED = (
0,
"Unplayed",
)
PLAYING = (1, "Playing")
DROPPED = (
2,
"Dropped",
)
FINISHED = (
3,
"Finished",
)
status = models.IntegerField(
choices=PurchaseState.choices, default=PurchaseState.UNPLAYED
)
def __str__(self): def __str__(self):
additional_info = [ additional_info = [
self.get_type_display() if self.type != Purchase.GAME else "", self.get_type_display() if self.type != Purchase.GAME else "",

View File

@ -43,11 +43,11 @@
</tr> </tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year_count }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td>
</tr> </tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year_count }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year.count }}</td>
</tr> </tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Longest session</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Longest session</td>
@ -63,14 +63,6 @@
{{ highest_session_average }} ({{ highest_session_average_game }}) {{ highest_session_average }} ({{ highest_session_average_game }})
</td> </td>
</tr> </tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">First play</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ first_play_name }} ({{ first_play_date }})</td>
</tr>
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Last play</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ last_play_name }} ({{ last_play_date }})</td>
</tr>
</tbody> </tbody>
</table> </table>
<h1 class="text-5xl text-center my-6">Purchases</h1> <h1 class="text-5xl text-center my-6">Purchases</h1>
@ -78,18 +70,18 @@
<tbody> <tbody>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Total</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Total</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_purchased_this_year_count }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_purchased_this_year.count }}</td>
</tr> </tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Refunded</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Refunded</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ all_purchased_refunded_this_year_count }} ({{ refunded_percent }}%) {{ all_purchased_refunded_this_year.count }} ({{ refunded_percent }}%)
</td> </td>
</tr> </tr>
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td> <td class="px-2 sm:px-4 md:px-6 md:py-2">Unfinished</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
{{ purchased_unfinished_count }} ({{ unfinished_purchases_percent }}%) {{ purchased_unfinished.count }} ({{ unfinished_purchases_percent }}%)
</td> </td>
</tr> </tr>
<tr> <tr>
@ -145,6 +137,7 @@
<tr> <tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th> <th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th> <th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Playtime</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -161,6 +154,7 @@
</a> </a>
</td> </td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.formatted_playtime }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -205,33 +199,6 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<h1 class="text-5xl text-center my-6">Unfinished Purchases</h1>
<table class="responsive-table">
<thead>
<tr>
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
</tr>
</thead>
<tbody>
{% for purchase in purchased_unfinished %}
<tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<a class="underline decoration-slate-500 sm:decoration-2"
href="{% url 'edit_purchase' purchase.id %}">
{{ purchase.edition.name }}
{% if purchase.type == "dlc" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %}
</a>
</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1 class="text-5xl text-center my-6">All Purchases</h1> <h1 class="text-5xl text-center my-6">All Purchases</h1>
<table class="responsive-table"> <table class="responsive-table">
<thead> <thead>

View File

@ -320,9 +320,7 @@ def stats(request, year: int = 0):
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 = timezone.now().year year = timezone.now().year
this_year_sessions = Session.objects.filter( this_year_sessions = Session.objects.filter(timestamp_start__year=year)
timestamp_start__year=year
).select_related("purchase__edition")
this_year_sessions_with_durations = this_year_sessions.annotate( this_year_sessions_with_durations = this_year_sessions.annotate(
duration=ExpressionWrapper( duration=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), F("timestamp_end") - F("timestamp_start"),
@ -330,10 +328,7 @@ def stats(request, year: int = 0):
) )
) )
longest_session = this_year_sessions_with_durations.order_by("-duration").first() longest_session = this_year_sessions_with_durations.order_by("-duration").first()
this_year_games = Game.objects.filter( this_year_games_with_session_counts = Game.objects.annotate(
edition__purchase__session__in=this_year_sessions
).distinct()
this_year_games_with_session_counts = this_year_games.annotate(
session_count=Count( session_count=Count(
"edition__purchase__session", "edition__purchase__session",
filter=Q(edition__purchase__session__timestamp_start__year=year), filter=Q(edition__purchase__session__timestamp_start__year=year),
@ -354,42 +349,48 @@ def stats(request, year: int = 0):
).distinct() ).distinct()
this_year_purchases = Purchase.objects.filter(date_purchased__year=year) this_year_purchases = Purchase.objects.filter(date_purchased__year=year)
this_year_purchases_with_currency = this_year_purchases.select_related( this_year_purchases_with_currency = this_year_purchases.filter(
"edition" price_currency__exact=selected_currency
).filter(price_currency__exact=selected_currency) )
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter( this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
date_refunded=None date_refunded=None
) )
this_year_purchases_refunded = this_year_purchases_with_currency.refunded() this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
this_year_purchases_unfinished = ( this_year_purchases_unfinished = this_year_purchases_without_refunded.filter(
this_year_purchases_without_refunded.filter(date_finished__isnull=True) date_finished__isnull=True
.filter(date_dropped__isnull=True) ).filter(
.filter(infinite=False) Q(type=Purchase.GAME) | Q(type=Purchase.DLC)
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
) # do not count battle passes etc. ) # do not count battle passes etc.
this_year_purchases_without_refunded_count = (
this_year_purchases_without_refunded.count()
)
this_year_purchases_unfinished_count = this_year_purchases_unfinished.count()
this_year_purchases_unfinished_percent = int( this_year_purchases_unfinished_percent = int(
safe_division( safe_division(
this_year_purchases_unfinished_count, this_year_purchases_unfinished.count(), this_year_purchases_refunded.count()
this_year_purchases_without_refunded_count,
) )
* 100 * 100
) )
purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year) purchases_finished_this_year = Purchase.objects.filter(date_finished__year=year)
purchases_finished_this_year_with_playtime = purchases_finished_this_year.annotate(
total_playtime=Sum(
F("session__duration_calculated") + F("session__duration_manual")
)
)
for purchase in purchases_finished_this_year_with_playtime:
formatted_playtime = format_duration(purchase.total_playtime, "%2.0H")
setattr(purchase, "formatted_playtime", formatted_playtime)
purchases_finished_this_year_released_this_year = ( purchases_finished_this_year_released_this_year = (
purchases_finished_this_year.filter(edition__year_released=year).order_by( purchases_finished_this_year.filter(edition__year_released=year).order_by(
"date_finished" "date_finished"
) )
) )
purchased_this_year_finished_this_year = ( purchased_this_year_finished_this_year = (
this_year_purchases_without_refunded.filter(date_finished__year=year) this_year_purchases_without_refunded.intersection(
).order_by("date_finished") purchases_finished_this_year
).order_by("date_finished")
)
this_year_spendings = this_year_purchases_without_refunded.aggregate( this_year_spendings = this_year_purchases_without_refunded.aggregate(
total_spent=Sum(F("price")) total_spent=Sum(F("price"))
@ -434,20 +435,6 @@ def stats(request, year: int = 0):
.count() .count()
) )
first_play_name = "N/A"
first_play_date = "N/A"
last_play_name = "N/A"
last_play_date = "N/A"
if this_year_sessions:
first_session = this_year_sessions.earliest()
first_play_name = first_session.purchase.edition.name
first_play_date = first_session.timestamp_start.strftime("%x")
last_session = this_year_sessions.latest()
last_play_name = last_session.purchase.edition.name
last_play_date = last_session.timestamp_start.strftime("%x")
all_purchased_this_year_count = this_year_purchases_with_currency.count()
all_purchased_refunded_this_year_count = this_year_purchases_refunded.count()
context = { context = {
"total_hours": format_duration( "total_hours": format_duration(
this_year_sessions.total_duration_unformatted(), "%2.0H" this_year_sessions.total_duration_unformatted(), "%2.0H"
@ -463,67 +450,45 @@ def stats(request, year: int = 0):
"total_spent_currency": selected_currency, "total_spent_currency": selected_currency,
"all_purchased_this_year": this_year_purchases_without_refunded, "all_purchased_this_year": this_year_purchases_without_refunded,
"spent_per_game": int( "spent_per_game": int(
safe_division(total_spent, this_year_purchases_without_refunded_count) safe_division(total_spent, this_year_purchases_without_refunded.count())
), ),
"all_finished_this_year": purchases_finished_this_year.select_related( "all_finished_this_year": purchases_finished_this_year_with_playtime.order_by(
"edition"
).order_by("date_finished"),
"all_finished_this_year_count": purchases_finished_this_year.count(),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related(
"edition"
).order_by(
"date_finished" "date_finished"
), ),
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), "this_year_finished_this_year": purchases_finished_this_year_released_this_year.order_by(
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related( "date_finished"
"edition" ),
).order_by( "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.order_by(
"date_finished" "date_finished"
), ),
"total_sessions": this_year_sessions.count(), "total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"], "unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100), "unique_days_percent": int(unique_days["dates"] / 365 * 100),
"purchased_unfinished": this_year_purchases_unfinished, "purchased_unfinished": this_year_purchases_unfinished,
"purchased_unfinished_count": this_year_purchases_unfinished_count,
"unfinished_purchases_percent": this_year_purchases_unfinished_percent, "unfinished_purchases_percent": this_year_purchases_unfinished_percent,
"refunded_percent": int( "refunded_percent": int(
safe_division( safe_division(
all_purchased_refunded_this_year_count, this_year_purchases_refunded.count(),
all_purchased_this_year_count, this_year_purchases_with_currency.count(),
) )
* 100 * 100
), ),
"all_purchased_refunded_this_year": this_year_purchases_refunded, "all_purchased_refunded_this_year": this_year_purchases_refunded,
"all_purchased_refunded_this_year_count": all_purchased_refunded_this_year_count,
"all_purchased_this_year": this_year_purchases_with_currency.order_by( "all_purchased_this_year": this_year_purchases_with_currency.order_by(
"date_purchased" "date_purchased"
), ),
"all_purchased_this_year_count": all_purchased_this_year_count,
"backlog_decrease_count": backlog_decrease_count, "backlog_decrease_count": backlog_decrease_count,
"longest_session_time": format_duration( "longest_session_time": format_duration(
longest_session.duration, "%2.0Hh %2.0mm" longest_session.duration if longest_session else timedelta(0),
) "%2.0Hh %2.0mm",
if longest_session ),
else 0, "longest_session_game": longest_session.purchase.edition.name,
"longest_session_game": longest_session.purchase.edition.name "highest_session_count": game_highest_session_count.session_count,
if longest_session "highest_session_count_game": game_highest_session_count.name,
else "N/A",
"highest_session_count": game_highest_session_count.session_count
if game_highest_session_count
else 0,
"highest_session_count_game": game_highest_session_count.name
if game_highest_session_count
else "N/A",
"highest_session_average": format_duration( "highest_session_average": format_duration(
highest_session_average_game.session_average, "%2.0Hh %2.0mm" highest_session_average_game.session_average, "%2.0Hh %2.0mm"
) ),
if highest_session_average_game
else 0,
"highest_session_average_game": highest_session_average_game, "highest_session_average_game": highest_session_average_game,
"first_play_name": first_play_name,
"first_play_date": first_play_date,
"last_play_name": last_play_name,
"last_play_date": last_play_date,
"title": f"{year} Stats", "title": f"{year} Stats",
} }