Compare commits
5 Commits
purchase_s
...
242833f886
Author | SHA1 | Date | |
---|---|---|---|
242833f886
|
|||
0cdfd3c298
|
|||
a98b4839dd
|
|||
1999f13cf2
|
|||
8466f67c86
|
@ -83,6 +83,7 @@ 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 = [
|
||||||
@ -91,6 +92,8 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
"date_purchased",
|
"date_purchased",
|
||||||
"date_refunded",
|
"date_refunded",
|
||||||
"date_finished",
|
"date_finished",
|
||||||
|
"date_dropped",
|
||||||
|
"infinite",
|
||||||
"price",
|
"price",
|
||||||
"price_currency",
|
"price_currency",
|
||||||
"ownership_type",
|
"ownership_type",
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
@ -116,6 +116,8 @@ 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(
|
||||||
|
@ -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,6 +63,14 @@
|
|||||||
{{ 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>
|
||||||
@ -70,18 +78,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>
|
||||||
@ -197,6 +205,33 @@
|
|||||||
{% 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>
|
||||||
|
103
games/views.py
103
games/views.py
@ -320,7 +320,9 @@ 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(timestamp_start__year=year)
|
this_year_sessions = Session.objects.filter(
|
||||||
|
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"),
|
||||||
@ -328,7 +330,10 @@ 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_with_session_counts = Game.objects.annotate(
|
this_year_games = Game.objects.filter(
|
||||||
|
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),
|
||||||
@ -349,23 +354,29 @@ 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.filter(
|
this_year_purchases_with_currency = this_year_purchases.select_related(
|
||||||
price_currency__exact=selected_currency
|
"edition"
|
||||||
)
|
).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_without_refunded.filter(
|
this_year_purchases_unfinished = (
|
||||||
date_finished__isnull=True
|
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
|
||||||
).filter(
|
.filter(date_dropped__isnull=True)
|
||||||
Q(type=Purchase.GAME) | Q(type=Purchase.DLC)
|
.filter(infinite=False)
|
||||||
|
.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_refunded.count()
|
this_year_purchases_unfinished_count,
|
||||||
|
this_year_purchases_without_refunded_count,
|
||||||
)
|
)
|
||||||
* 100
|
* 100
|
||||||
)
|
)
|
||||||
@ -377,10 +388,8 @@ def stats(request, year: int = 0):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
purchased_this_year_finished_this_year = (
|
purchased_this_year_finished_this_year = (
|
||||||
this_year_purchases_without_refunded.intersection(
|
this_year_purchases_without_refunded.filter(date_finished__year=year)
|
||||||
purchases_finished_this_year
|
).order_by("date_finished")
|
||||||
).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"))
|
||||||
@ -425,6 +434,20 @@ 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"
|
||||||
@ -440,45 +463,67 @@ 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.order_by(
|
"all_finished_this_year": purchases_finished_this_year.select_related(
|
||||||
|
"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": purchases_finished_this_year_released_this_year.order_by(
|
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
|
||||||
"date_finished"
|
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related(
|
||||||
),
|
"edition"
|
||||||
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.order_by(
|
).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(
|
||||||
this_year_purchases_refunded.count(),
|
all_purchased_refunded_this_year_count,
|
||||||
this_year_purchases_with_currency.count(),
|
all_purchased_this_year_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 if longest_session else timedelta(0),
|
longest_session.duration, "%2.0Hh %2.0mm"
|
||||||
"%2.0Hh %2.0mm",
|
)
|
||||||
),
|
if longest_session
|
||||||
"longest_session_game": longest_session.purchase.edition.name,
|
else 0,
|
||||||
"highest_session_count": game_highest_session_count.session_count,
|
"longest_session_game": longest_session.purchase.edition.name
|
||||||
"highest_session_count_game": game_highest_session_count.name,
|
if longest_session
|
||||||
|
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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user