Compare commits
No commits in common. "714f0d97a9283a321a10932a373b428cf678cd3e" and "86fd40cc4ac5c5aa63d3c14fa54e8c71a741cc5f" have entirely different histories.
714f0d97a9
...
86fd40cc4a
|
@ -5,7 +5,6 @@
|
||||||
* Require login by default
|
* Require login by default
|
||||||
* Add stats for dropped purchases, monthly playtimes
|
* Add stats for dropped purchases, monthly playtimes
|
||||||
* Allow deleting purchases
|
* Allow deleting purchases
|
||||||
* Add all-time stats
|
|
||||||
|
|
||||||
## Improved
|
## Improved
|
||||||
* mark refunded purchases red on game overview
|
* mark refunded purchases red on game overview
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
|
! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1246,6 +1246,18 @@ input:checked + .toggle-bg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
@ -1374,6 +1386,10 @@ input:checked + .toggle-bg {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.me-2 {
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ml-1 {
|
.ml-1 {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -1435,10 +1451,6 @@ input:checked + .toggle-bg {
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-3 {
|
|
||||||
height: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-4 {
|
.h-4 {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -1495,6 +1507,14 @@ input:checked + .toggle-bg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-14 {
|
||||||
|
min-width: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-24 {
|
||||||
|
max-width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-80 {
|
.max-w-80 {
|
||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
}
|
}
|
||||||
|
@ -1794,6 +1814,10 @@ input:checked + .toggle-bg {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.p-2\.5 {
|
.p-2\.5 {
|
||||||
padding: 0.625rem;
|
padding: 0.625rem;
|
||||||
}
|
}
|
||||||
|
@ -2531,6 +2555,11 @@ th label {
|
||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group:hover .group-hover\:py-3 {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.group:hover .group-hover\:py-3\.5 {
|
.group:hover .group-hover\:py-3\.5 {
|
||||||
padding-top: 0.875rem;
|
padding-top: 0.875rem;
|
||||||
padding-bottom: 0.875rem;
|
padding-bottom: 0.875rem;
|
||||||
|
@ -2704,6 +2733,11 @@ th label {
|
||||||
--tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity));
|
--tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark\:focus\:ring-blue-800:focus:is(.dark *) {
|
||||||
|
--tw-ring-opacity: 1;
|
||||||
|
--tw-ring-color: rgb(30 66 159 / var(--tw-ring-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.sm\:inline {
|
.sm\:inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
|
@ -81,12 +81,8 @@
|
||||||
{% if session_count > 0 %}
|
{% if session_count > 0 %}
|
||||||
<li class="relative group">
|
<li class="relative group">
|
||||||
<a class="block py-2 pl-3 pr-4 hover:underline"
|
<a class="block py-2 pl-3 pr-4 hover:underline"
|
||||||
href="{% url 'stats_by_year' 0 %}">Stats</a>
|
href="{% url 'stats_current_year' %}">Stats</a>
|
||||||
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block">
|
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block">
|
||||||
<li>
|
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
|
||||||
href="{% url 'stats_by_year' 0 %}">Overall</a>
|
|
||||||
</li>
|
|
||||||
{% for year in stats_dropdown_year_range %}
|
{% for year in stats_dropdown_year_range %}
|
||||||
<li>
|
<li>
|
||||||
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
|
||||||
|
|
|
@ -44,22 +44,18 @@
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Days</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Days</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ unique_days }} ({{ unique_days_percent }}%)</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ unique_days }} ({{ unique_days_percent }}%)</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if total_games %}
|
<tr>
|
||||||
<tr>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_games }}</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Games ({{ year }})</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_2023_games }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if all_finished_this_year_count %}
|
<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>
|
|
||||||
{% endif %}
|
|
||||||
<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>
|
||||||
|
@ -89,19 +85,17 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if month_playtime %}
|
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
|
||||||
<h1 class="text-5xl text-center my-6">Playtime per month</h1>
|
<table class="responsive-table">
|
||||||
<table class="responsive-table">
|
<tbody>
|
||||||
<tbody>
|
{% for month in month_playtimes %}
|
||||||
{% for month in month_playtimes %}
|
<tr>
|
||||||
<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">{{ 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 }}</td>
|
</tr>
|
||||||
</tr>
|
{% endfor %}
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h1 class="text-5xl text-center my-6">Purchases</h1>
|
<h1 class="text-5xl text-center my-6">Purchases</h1>
|
||||||
<table class="responsive-table">
|
<table class="responsive-table">
|
||||||
|
@ -174,119 +168,106 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h1 class="text-5xl text-center my-6">Finished</h1>
|
||||||
{% if all_finished_this_year %}
|
<table class="responsive-table">
|
||||||
<h1 class="text-5xl text-center my-6">Finished</h1>
|
<thead>
|
||||||
<table class="responsive-table">
|
<tr>
|
||||||
<thead>
|
<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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for purchase in all_finished_this_year %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
{% partial purchase-name %}
|
||||||
|
</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for purchase in all_finished_this_year %}
|
</table>
|
||||||
<tr>
|
<h1 class="text-5xl text-center my-6">Finished ({{ year }} games)</h1>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<table class="responsive-table">
|
||||||
{% partial purchase-name %}
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
||||||
</tr>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
{% endif %}
|
{% for purchase in this_year_finished_this_year %}
|
||||||
|
|
||||||
{% if this_year_finished_this_year %}
|
|
||||||
<h1 class="text-5xl text-center my-6">Finished ({{ year }} games)</h1>
|
|
||||||
<table class="responsive-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
{% partial purchase-name %}
|
||||||
|
</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for purchase in this_year_finished_this_year %}
|
</table>
|
||||||
<tr>
|
<h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</h1>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<table class="responsive-table">
|
||||||
{% partial purchase-name %}
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
||||||
</tr>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
</thead>
|
||||||
</table>
|
<tbody>
|
||||||
{% endif %}
|
{% for purchase in purchased_this_year_finished_this_year %}
|
||||||
|
|
||||||
{% if purchased_this_year_finished_this_year %}
|
|
||||||
<h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</h1>
|
|
||||||
<table class="responsive-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
{% partial purchase-name %}
|
||||||
|
</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_finished | date:"d/m/Y" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for purchase in purchased_this_year_finished_this_year %}
|
</table>
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
|
||||||
{% partial purchase-name %}
|
|
||||||
</td>
|
|
||||||
<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>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if purchased_unfinished %}
|
<h1 class="text-5xl text-center my-6">Unfinished Purchases</h1>
|
||||||
<h1 class="text-5xl text-center my-6">Unfinished Purchases</h1>
|
<table class="responsive-table">
|
||||||
<table class="responsive-table">
|
<thead>
|
||||||
<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>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
|
{% partial purchase-name %}
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
</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>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for purchase in purchased_unfinished %}
|
</table>
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
|
||||||
{% partial purchase-name %}
|
|
||||||
</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>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if all_purchased_this_year %}
|
<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>
|
<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 all_purchased_this_year %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char">Name</th>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Price ({{ total_spent_currency }})</th>
|
{% partial purchase-name %}
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Date</th>
|
</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>
|
</tr>
|
||||||
</thead>
|
{% endfor %}
|
||||||
<tbody>
|
</tbody>
|
||||||
{% for purchase in all_purchased_this_year %}
|
</table>
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
|
||||||
{% partial purchase-name %}
|
|
||||||
</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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
<div class="flex gap-5 mb-3">
|
<div class="flex gap-5 mb-3">
|
||||||
<span class="text-wrap max-w-80 text-4xl">
|
<span class="text-wrap max-w-80 text-4xl">
|
||||||
<span class="font-bold font-serif">{{ game.name }}</span> <span data-popover-target="popover-year" class="text-slate-500 text-2xl">{{ game.year_released }}</span>
|
<span class="font-bold font-serif">{{ game.name }}</span> <span data-popover-target="popover-year" class="text-slate-500 text-2xl">{{ game.year_released }}</span>
|
||||||
{% #popover id="popover-year" %}
|
{% #popover id="popover-year" %}
|
||||||
Original release year
|
Original release year
|
||||||
{% /popover %}
|
{% /popover %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 dark:text-slate-400 mb-3">
|
<div class="flex gap-4 dark:text-slate-400 mb-3">
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ hours_sum }}
|
{{ hours_sum }}
|
||||||
{% #popover id="popover-hours" %}
|
{% #popover id="popover-hours" %}
|
||||||
Total hours played
|
Total hours played
|
||||||
{% /popover %}
|
{% /popover %}
|
||||||
</span>
|
</span>
|
||||||
<span data-popover-target="popover-sessions" class="flex gap-2 items-center">
|
<span data-popover-target="popover-sessions" class="flex gap-2 items-center">
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ session_count }}
|
{{ session_count }}
|
||||||
{% #popover id="popover-sessions" %}
|
{% #popover id="popover-sessions" %}
|
||||||
Number of sessions
|
Number of sessions
|
||||||
{% /popover %}
|
{% /popover %}
|
||||||
</span>
|
</span>
|
||||||
<span data-popover-target="popover-average" class="flex gap-2 items-center">
|
<span data-popover-target="popover-average" class="flex gap-2 items-center">
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ session_average_without_manual }}
|
{{ session_average_without_manual }}
|
||||||
{% #popover id="popover-average" %}
|
{% #popover id="popover-average" %}
|
||||||
Average playtime per session
|
Average playtime per session
|
||||||
{% /popover %}
|
{% /popover %}
|
||||||
</span>
|
</span>
|
||||||
<span data-popover-target="popover-playrange" class="flex gap-2 items-center">
|
<span data-popover-target="popover-playrange" class="flex gap-2 items-center">
|
||||||
|
@ -49,19 +49,19 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{ playrange }}
|
{{ playrange }}
|
||||||
{% #popover id="popover-playrange" %}
|
{% #popover id="popover-playrange" %}
|
||||||
Earliest and latest dates played
|
Earliest and latest dates played
|
||||||
{% /popover %}
|
{% /popover %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
||||||
<a href="{% url 'edit_game' game.id %}">
|
<a href="{% url 'edit_game' game.id %}">
|
||||||
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-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">
|
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-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">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'delete_game' game.id %}">
|
<a href="{% url 'delete_game' game.id %}">
|
||||||
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-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-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
|
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-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-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,18 +108,18 @@
|
||||||
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center font-condensed">
|
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center font-condensed">
|
||||||
Sessions
|
Sessions
|
||||||
<span class="dark:text-slate-500" id="session-count">({{ session_count }})</span>
|
<span class="dark:text-slate-500" id="session-count">({{ session_count }})</span>
|
||||||
{% if latest_session_id %}
|
{% if latest_session_id %}
|
||||||
{% url 'view_game_start_session_from_session' latest_session_id as add_session_link %}
|
{% url 'view_game_start_session_from_session' latest_session_id as add_session_link %}
|
||||||
<a
|
<a
|
||||||
class="truncate max-w-xs py-1 px-2 text-xs bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-sm"
|
class="truncate max-w-xs py-1 px-2 text-xs bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-sm"
|
||||||
title="Start new session"
|
title="Start new session"
|
||||||
href="{{ add_session_link }}"
|
href="{{ add_session_link }}"
|
||||||
hx-get="{{ add_session_link }}"
|
hx-get="{{ add_session_link }}"
|
||||||
hx-vals="js:{session_count:getSessionCount()}"
|
hx-vals="js:{session_count:getSessionCount()}"
|
||||||
hx-target="#session-list"
|
hx-target="#session-list"
|
||||||
hx-swap="afterbegin"
|
hx-swap="afterbegin"
|
||||||
>New</a>
|
>New</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
and Notes <span class="dark:text-slate-500">({{ sessions_with_notes_count }})</span>
|
and Notes <span class="dark:text-slate-500">({{ sessions_with_notes_count }})</span>
|
||||||
</h1>
|
</h1>
|
||||||
<ul id="session-list">
|
<ul id="session-list">
|
||||||
|
|
|
@ -108,7 +108,7 @@ urlpatterns = [
|
||||||
{"filter": "ownership_type"},
|
{"filter": "ownership_type"},
|
||||||
name="list_sessions_by_ownership_type",
|
name="list_sessions_by_ownership_type",
|
||||||
),
|
),
|
||||||
path("stats/", views.stats_alltime, name="stats_alltime"),
|
path("stats/", views.stats, name="stats_current_year"),
|
||||||
path(
|
path(
|
||||||
"stats/<int:year>",
|
"stats/<int:year>",
|
||||||
views.stats,
|
views.stats,
|
||||||
|
|
216
games/views.py
216
games/views.py
|
@ -365,227 +365,13 @@ def list_sessions(
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def stats_alltime(request):
|
|
||||||
year = "Alltime"
|
|
||||||
this_year_sessions = Session.objects.all().select_related("purchase__edition")
|
|
||||||
this_year_sessions_with_durations = this_year_sessions.annotate(
|
|
||||||
duration=ExpressionWrapper(
|
|
||||||
F("timestamp_end") - F("timestamp_start"),
|
|
||||||
output_field=fields.DurationField(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
longest_session = this_year_sessions_with_durations.order_by("-duration").first()
|
|
||||||
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("edition__purchase__session"),
|
|
||||||
)
|
|
||||||
game_highest_session_count = this_year_games_with_session_counts.order_by(
|
|
||||||
"-session_count"
|
|
||||||
).first()
|
|
||||||
selected_currency = "CZK"
|
|
||||||
unique_days = (
|
|
||||||
this_year_sessions.annotate(date=TruncDate("timestamp_start"))
|
|
||||||
.values("date")
|
|
||||||
.distinct()
|
|
||||||
.aggregate(dates=Count("date"))
|
|
||||||
)
|
|
||||||
this_year_played_purchases = Purchase.objects.filter(
|
|
||||||
session__in=this_year_sessions
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
this_year_purchases = Purchase.objects.all()
|
|
||||||
this_year_purchases_with_currency = this_year_purchases.select_related(
|
|
||||||
"edition"
|
|
||||||
).filter(price_currency__exact=selected_currency)
|
|
||||||
this_year_purchases_without_refunded = this_year_purchases_with_currency.filter(
|
|
||||||
date_refunded=None
|
|
||||||
)
|
|
||||||
this_year_purchases_refunded = this_year_purchases_with_currency.refunded()
|
|
||||||
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped = (
|
|
||||||
this_year_purchases_without_refunded.filter(date_finished__isnull=True)
|
|
||||||
.filter(infinite=False)
|
|
||||||
.filter(Q(type=Purchase.GAME) | Q(type=Purchase.DLC))
|
|
||||||
) # do not count battle passes etc.
|
|
||||||
|
|
||||||
this_year_purchases_unfinished = (
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
|
||||||
date_dropped__isnull=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
this_year_purchases_dropped = (
|
|
||||||
this_year_purchases_unfinished_dropped_nondropped.filter(
|
|
||||||
date_dropped__isnull=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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(
|
|
||||||
safe_division(
|
|
||||||
this_year_purchases_unfinished_count,
|
|
||||||
this_year_purchases_without_refunded_count,
|
|
||||||
)
|
|
||||||
* 100
|
|
||||||
)
|
|
||||||
|
|
||||||
purchases_finished_this_year = Purchase.objects.finished()
|
|
||||||
purchases_finished_this_year_released_this_year = (
|
|
||||||
purchases_finished_this_year.all().order_by("date_finished")
|
|
||||||
)
|
|
||||||
purchased_this_year_finished_this_year = (
|
|
||||||
this_year_purchases_without_refunded.all()
|
|
||||||
).order_by("date_finished")
|
|
||||||
|
|
||||||
this_year_spendings = this_year_purchases_without_refunded.aggregate(
|
|
||||||
total_spent=Sum(F("price"))
|
|
||||||
)
|
|
||||||
total_spent = this_year_spendings["total_spent"] or 0
|
|
||||||
|
|
||||||
games_with_playtime = (
|
|
||||||
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
|
||||||
.annotate(
|
|
||||||
total_playtime=Sum(
|
|
||||||
F("edition__purchase__session__duration_calculated")
|
|
||||||
+ F("edition__purchase__session__duration_manual")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.values("id", "name", "total_playtime")
|
|
||||||
)
|
|
||||||
month_playtimes = (
|
|
||||||
this_year_sessions.annotate(month=TruncMonth("timestamp_start"))
|
|
||||||
.values("month")
|
|
||||||
.annotate(playtime=Sum("duration_calculated"))
|
|
||||||
.order_by("month")
|
|
||||||
)
|
|
||||||
for month in month_playtimes:
|
|
||||||
month["playtime"] = format_duration(month["playtime"], "%2.0H")
|
|
||||||
|
|
||||||
highest_session_average_game = (
|
|
||||||
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
|
||||||
.annotate(
|
|
||||||
session_average=Avg("edition__purchase__session__duration_calculated")
|
|
||||||
)
|
|
||||||
.order_by("-session_average")
|
|
||||||
.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("purchase__platform__name")
|
|
||||||
.annotate(total_playtime=Sum(F("duration_calculated") + F("duration_manual")))
|
|
||||||
.annotate(platform_name=F("purchase__platform__name"))
|
|
||||||
.values("platform_name", "total_playtime")
|
|
||||||
.order_by("-total_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()
|
|
||||||
)
|
|
||||||
|
|
||||||
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_game = first_session.purchase.edition.game
|
|
||||||
first_play_date = first_session.timestamp_start.strftime("%x")
|
|
||||||
last_session = this_year_sessions.latest()
|
|
||||||
last_play_game = last_session.purchase.edition.game
|
|
||||||
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()
|
|
||||||
|
|
||||||
this_year_purchases_dropped_count = this_year_purchases_dropped.count()
|
|
||||||
this_year_purchases_dropped_percentage = int(
|
|
||||||
safe_division(this_year_purchases_dropped_count, all_purchased_this_year_count)
|
|
||||||
* 100
|
|
||||||
)
|
|
||||||
context = {
|
|
||||||
"total_hours": format_duration(
|
|
||||||
this_year_sessions.total_duration_unformatted(), "%2.0H"
|
|
||||||
),
|
|
||||||
"total_2023_games": this_year_played_purchases.all().count(),
|
|
||||||
"top_10_games_by_playtime": top_10_games_by_playtime,
|
|
||||||
"year": year,
|
|
||||||
"total_playtime_per_platform": total_playtime_per_platform,
|
|
||||||
"total_spent": total_spent,
|
|
||||||
"total_spent_currency": selected_currency,
|
|
||||||
"spent_per_game": int(
|
|
||||||
safe_division(total_spent, this_year_purchases_without_refunded_count)
|
|
||||||
),
|
|
||||||
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
|
|
||||||
"total_sessions": this_year_sessions.count(),
|
|
||||||
"unique_days": unique_days["dates"],
|
|
||||||
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
|
|
||||||
"purchased_unfinished_count": this_year_purchases_unfinished_count,
|
|
||||||
"unfinished_purchases_percent": this_year_purchases_unfinished_percent,
|
|
||||||
"dropped_count": this_year_purchases_dropped_count,
|
|
||||||
"dropped_percentage": this_year_purchases_dropped_percentage,
|
|
||||||
"refunded_percent": int(
|
|
||||||
safe_division(
|
|
||||||
all_purchased_refunded_this_year_count,
|
|
||||||
all_purchased_this_year_count,
|
|
||||||
)
|
|
||||||
* 100
|
|
||||||
),
|
|
||||||
"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_count": all_purchased_this_year_count,
|
|
||||||
"backlog_decrease_count": backlog_decrease_count,
|
|
||||||
"longest_session_time": (
|
|
||||||
format_duration(longest_session.duration, "%2.0Hh %2.0mm")
|
|
||||||
if longest_session
|
|
||||||
else 0
|
|
||||||
),
|
|
||||||
"longest_session_game": (
|
|
||||||
longest_session.purchase.edition.game if longest_session else None
|
|
||||||
),
|
|
||||||
"highest_session_count": (
|
|
||||||
game_highest_session_count.session_count
|
|
||||||
if game_highest_session_count
|
|
||||||
else 0
|
|
||||||
),
|
|
||||||
"highest_session_count_game": (
|
|
||||||
game_highest_session_count if game_highest_session_count else None
|
|
||||||
),
|
|
||||||
"highest_session_average": (
|
|
||||||
format_duration(
|
|
||||||
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,
|
|
||||||
"first_play_game": first_play_game,
|
|
||||||
"first_play_date": first_play_date,
|
|
||||||
"last_play_game": last_play_game,
|
|
||||||
"last_play_date": last_play_date,
|
|
||||||
"title": f"{year} Stats",
|
|
||||||
}
|
|
||||||
|
|
||||||
request.session["return_path"] = request.path
|
|
||||||
return render(request, "stats.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def stats(request, year: int = 0):
|
def stats(request, year: int = 0):
|
||||||
selected_year = request.GET.get("year")
|
selected_year = request.GET.get("year")
|
||||||
if selected_year:
|
if selected_year:
|
||||||
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
|
return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year]))
|
||||||
if year == 0:
|
if year == 0:
|
||||||
return HttpResponseRedirect(reverse("stats_alltime"))
|
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")
|
).select_related("purchase__edition")
|
||||||
|
|
Loading…
Reference in New Issue