Compare commits
	
		
			2 Commits
		
	
	
		
			86fd40cc4a
			...
			714f0d97a9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 714f0d97a9 | |||
| d622ddfbf3 | 
| @ -5,6 +5,7 @@ | ||||
| * Require login by default | ||||
| * Add stats for dropped purchases, monthly playtimes | ||||
| * Allow deleting purchases | ||||
| * Add all-time stats | ||||
|  | ||||
| ## Improved | ||||
| * mark refunded purchases red on game overview | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| /* | ||||
| ! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com | ||||
| ! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com | ||||
| */ | ||||
|  | ||||
| /* | ||||
| @ -1246,18 +1246,6 @@ 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 { | ||||
|   visibility: visible; | ||||
| } | ||||
| @ -1386,10 +1374,6 @@ input:checked + .toggle-bg { | ||||
|   margin-bottom: 2rem; | ||||
| } | ||||
|  | ||||
| .me-2 { | ||||
|   margin-inline-end: 0.5rem; | ||||
| } | ||||
|  | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
| @ -1451,6 +1435,10 @@ input:checked + .toggle-bg { | ||||
|   height: 6rem; | ||||
| } | ||||
|  | ||||
| .h-3 { | ||||
|   height: 0.75rem; | ||||
| } | ||||
|  | ||||
| .h-4 { | ||||
|   height: 1rem; | ||||
| } | ||||
| @ -1507,14 +1495,6 @@ input:checked + .toggle-bg { | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .min-w-14 { | ||||
|   min-width: 3.5rem; | ||||
| } | ||||
|  | ||||
| .max-w-24 { | ||||
|   max-width: 6rem; | ||||
| } | ||||
|  | ||||
| .max-w-80 { | ||||
|   max-width: 20rem; | ||||
| } | ||||
| @ -1814,10 +1794,6 @@ input:checked + .toggle-bg { | ||||
|   padding: 0.25rem; | ||||
| } | ||||
|  | ||||
| .p-2 { | ||||
|   padding: 0.5rem; | ||||
| } | ||||
|  | ||||
| .p-2\.5 { | ||||
|   padding: 0.625rem; | ||||
| } | ||||
| @ -2555,11 +2531,6 @@ th label { | ||||
|   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 { | ||||
|   padding-top: 0.875rem; | ||||
|   padding-bottom: 0.875rem; | ||||
| @ -2733,11 +2704,6 @@ th label { | ||||
|   --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) { | ||||
|   .sm\:inline { | ||||
|     display: inline; | ||||
|  | ||||
| @ -81,8 +81,12 @@ | ||||
|                                     {% if session_count > 0 %} | ||||
|                                         <li class="relative group"> | ||||
|                                             <a class="block py-2 pl-3 pr-4 hover:underline" | ||||
|                                                href="{% url 'stats_current_year' %}">Stats</a> | ||||
|                                                href="{% url 'stats_by_year' 0 %}">Stats</a> | ||||
|                                             <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 %} | ||||
|                                                     <li> | ||||
|                                                         <a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap" | ||||
|  | ||||
| @ -44,18 +44,22 @@ | ||||
|                     <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> | ||||
|                 </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 font-mono">{{ total_games }}</td> | ||||
|                 </tr> | ||||
|                 {% if total_games %} | ||||
|                     <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 font-mono">{{ total_games }}</td> | ||||
|                     </tr> | ||||
|                 {% endif %} | ||||
|                 <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 font-mono">{{ total_2023_games }}</td> | ||||
|                 </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 font-mono">{{ all_finished_this_year_count }}</td> | ||||
|                 </tr> | ||||
|                 {% if all_finished_this_year_count %} | ||||
|                     <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 font-mono">{{ all_finished_this_year_count }}</td> | ||||
|                     </tr> | ||||
|                 {% endif %} | ||||
|                 <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 font-mono">{{ this_year_finished_this_year_count }}</td> | ||||
| @ -85,17 +89,19 @@ | ||||
|             </tbody> | ||||
|         </table> | ||||
|  | ||||
|         <h1 class="text-5xl text-center my-6">Playtime per month</h1> | ||||
|         <table class="responsive-table"> | ||||
|             <tbody> | ||||
|                 {% 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> | ||||
|                     </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         {% if month_playtime %} | ||||
|             <h1 class="text-5xl text-center my-6">Playtime per month</h1> | ||||
|             <table class="responsive-table"> | ||||
|                 <tbody> | ||||
|                     {% 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> | ||||
|                         </tr> | ||||
|                     {% endfor %} | ||||
|                 </tbody> | ||||
|             </table> | ||||
|         {% endif %} | ||||
|  | ||||
|         <h1 class="text-5xl text-center my-6">Purchases</h1> | ||||
|         <table class="responsive-table"> | ||||
| @ -168,106 +174,119 @@ | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <h1 class="text-5xl text-center my-6">Finished</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">Date</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 {% for purchase in all_finished_this_year %} | ||||
|                     <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> | ||||
|         <h1 class="text-5xl text-center my-6">Finished ({{ year }} games)</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">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> | ||||
|                         <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> | ||||
|         <h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</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">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> | ||||
|                         <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> | ||||
|  | ||||
|         <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 %} | ||||
|         {% if all_finished_this_year %} | ||||
|             <h1 class="text-5xl text-center my-6">Finished</h1> | ||||
|             <table class="responsive-table"> | ||||
|                 <thead> | ||||
|                     <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> | ||||
|                         <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> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                     {% for purchase in all_finished_this_year %} | ||||
|                         <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 %} | ||||
|  | ||||
|         <h1 class="text-5xl text-center my-6">All 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 all_purchased_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> | ||||
|                         <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> | ||||
|                         <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> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|                 </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> | ||||
|                             <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_this_year_finished_this_year %} | ||||
|             <h1 class="text-5xl text-center my-6">Bought and Finished ({{ year }})</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">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> | ||||
|                             <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> | ||||
|             <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"> | ||||
|                                 {% 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> | ||||
|             <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 all_purchased_this_year %} | ||||
|                         <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> | ||||
| {% endblock content %} | ||||
|  | ||||
| @ -10,9 +10,9 @@ | ||||
|             <div class="flex gap-5 mb-3"> | ||||
|                 <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> | ||||
|                         {% #popover id="popover-year" %} | ||||
|                             Original release year | ||||
|                         {% /popover %} | ||||
|                     {% #popover id="popover-year" %} | ||||
|                         Original release year | ||||
|                     {% /popover %} | ||||
|                 </span> | ||||
|             </div> | ||||
|             <div class="flex gap-4 dark:text-slate-400 mb-3"> | ||||
| @ -22,7 +22,7 @@ | ||||
|                     </svg> | ||||
|                     {{ hours_sum }} | ||||
|                     {% #popover id="popover-hours" %} | ||||
|                     Total hours played | ||||
|                         Total hours played | ||||
|                     {% /popover %} | ||||
|                 </span> | ||||
|                 <span data-popover-target="popover-sessions" class="flex gap-2 items-center"> | ||||
| @ -31,7 +31,7 @@ | ||||
|                     </svg> | ||||
|                     {{ session_count }} | ||||
|                     {% #popover id="popover-sessions" %} | ||||
|                     Number of sessions | ||||
|                         Number of sessions | ||||
|                     {% /popover %} | ||||
|                 </span> | ||||
|                 <span data-popover-target="popover-average" class="flex gap-2 items-center"> | ||||
| @ -40,7 +40,7 @@ | ||||
|                     </svg> | ||||
|                     {{ session_average_without_manual }} | ||||
|                     {% #popover id="popover-average" %} | ||||
|                     Average playtime per session | ||||
|                         Average playtime per session | ||||
|                     {% /popover %} | ||||
|                 </span> | ||||
|                 <span data-popover-target="popover-playrange" class="flex gap-2 items-center"> | ||||
| @ -49,19 +49,19 @@ | ||||
|                     </svg> | ||||
|                     {{ playrange }} | ||||
|                     {% #popover id="popover-playrange" %} | ||||
|                     Earliest and latest dates played | ||||
|                         Earliest and latest dates played | ||||
|                     {% /popover %} | ||||
|                 </span> | ||||
|             </div> | ||||
|             <div class="inline-flex rounded-md shadow-sm mb-3" role="group"> | ||||
|                 <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"> | ||||
|                     Edit | ||||
|                         Edit | ||||
|                     </button> | ||||
|                 </a> | ||||
|                 <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"> | ||||
|                     Delete | ||||
|                         Delete | ||||
|                     </button> | ||||
|                 </a> | ||||
|             </div> | ||||
| @ -108,18 +108,18 @@ | ||||
|         <h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center font-condensed"> | ||||
|             Sessions | ||||
|             <span class="dark:text-slate-500" id="session-count">({{ session_count }})</span> | ||||
| {% if latest_session_id %} | ||||
|             {% url 'view_game_start_session_from_session' latest_session_id as add_session_link %} | ||||
|             <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" | ||||
|                 title="Start new session" | ||||
|                 href="{{ add_session_link }}" | ||||
|                 hx-get="{{ add_session_link }}" | ||||
|                 hx-vals="js:{session_count:getSessionCount()}" | ||||
|                 hx-target="#session-list" | ||||
|                 hx-swap="afterbegin" | ||||
|             >New</a> | ||||
| {% endif %} | ||||
|             {% if latest_session_id %} | ||||
|                 {% url 'view_game_start_session_from_session' latest_session_id as add_session_link %} | ||||
|                 <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" | ||||
|                     title="Start new session" | ||||
|                     href="{{ add_session_link }}" | ||||
|                     hx-get="{{ add_session_link }}" | ||||
|                     hx-vals="js:{session_count:getSessionCount()}" | ||||
|                     hx-target="#session-list" | ||||
|                     hx-swap="afterbegin" | ||||
|                 >New</a> | ||||
|             {% endif %} | ||||
|             and Notes <span class="dark:text-slate-500">({{ sessions_with_notes_count }})</span> | ||||
|         </h1> | ||||
|         <ul id="session-list"> | ||||
|  | ||||
| @ -108,7 +108,7 @@ urlpatterns = [ | ||||
|         {"filter": "ownership_type"}, | ||||
|         name="list_sessions_by_ownership_type", | ||||
|     ), | ||||
|     path("stats/", views.stats, name="stats_current_year"), | ||||
|     path("stats/", views.stats_alltime, name="stats_alltime"), | ||||
|     path( | ||||
|         "stats/<int:year>", | ||||
|         views.stats, | ||||
|  | ||||
							
								
								
									
										216
									
								
								games/views.py
									
									
									
									
									
								
							
							
						
						
									
										216
									
								
								games/views.py
									
									
									
									
									
								
							| @ -365,13 +365,227 @@ def list_sessions( | ||||
|     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 | ||||
| def stats(request, year: int = 0): | ||||
|     selected_year = request.GET.get("year") | ||||
|     if selected_year: | ||||
|         return HttpResponseRedirect(reverse("stats_by_year", args=[selected_year])) | ||||
|     if year == 0: | ||||
|         year = timezone.now().year | ||||
|         return HttpResponseRedirect(reverse("stats_alltime")) | ||||
|     this_year_sessions = Session.objects.filter( | ||||
|         timestamp_start__year=year | ||||
|     ).select_related("purchase__edition") | ||||
|  | ||||
		Reference in New Issue
	
	Block a user