Improve game overview
- add counts for each section - add average hours per session
This commit is contained in:
		| @ -36,9 +36,9 @@ def format_duration( | ||||
|     minute_seconds = 60 | ||||
|     hour_seconds = 60 * minute_seconds | ||||
|     day_seconds = 24 * hour_seconds | ||||
|     duration = _safe_timedelta(duration) | ||||
|     safe_duration = _safe_timedelta(duration) | ||||
|     # we don't need float | ||||
|     seconds_total = int(duration.total_seconds()) | ||||
|     seconds_total = int(safe_duration.total_seconds()) | ||||
|     # timestamps where end is before start | ||||
|     if seconds_total < 0: | ||||
|         seconds_total = 0 | ||||
|  | ||||
| @ -73,11 +73,14 @@ class Platform(models.Model): | ||||
|  | ||||
|  | ||||
| class SessionQuerySet(models.QuerySet): | ||||
|     def total_duration(self): | ||||
|     def total_duration_formatted(self): | ||||
|         return format_duration(self.total_duration_unformatted()) | ||||
|  | ||||
|     def total_duration_unformatted(self): | ||||
|         result = self.aggregate( | ||||
|             duration=Sum(F("duration_calculated") + F("duration_manual")) | ||||
|         ) | ||||
|         return format_duration(result["duration"]) | ||||
|         return result["duration"] | ||||
|  | ||||
|  | ||||
| class Session(models.Model): | ||||
| @ -116,7 +119,7 @@ class Session(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def duration_sum(self) -> str: | ||||
|         return Session.objects.all().total_duration() | ||||
|         return Session.objects.all().total_duration_formatted() | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.timestamp_start != None and self.timestamp_end != None: | ||||
|  | ||||
| @ -771,24 +771,14 @@ select { | ||||
|   top: 0.75rem; | ||||
| } | ||||
|  | ||||
| .mx-auto { | ||||
|   margin-left: auto; | ||||
|   margin-right: auto; | ||||
| } | ||||
|  | ||||
| .my-4 { | ||||
|   margin-top: 1rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .mx-2 { | ||||
|   margin-left: 0.5rem; | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
|  | ||||
| .my-1 { | ||||
|   margin-top: 0.25rem; | ||||
|   margin-bottom: 0.25rem; | ||||
| .mx-auto { | ||||
|   margin-left: auto; | ||||
|   margin-right: auto; | ||||
| } | ||||
|  | ||||
| .my-2 { | ||||
| @ -796,10 +786,23 @@ select { | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
|  | ||||
| .my-4 { | ||||
|   margin-top: 1rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .mb-1 { | ||||
|   margin-bottom: 0.25rem; | ||||
| } | ||||
|  | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|  | ||||
| .ml-2 { | ||||
|   margin-left: 0.5rem; | ||||
| } | ||||
|  | ||||
| .mr-4 { | ||||
|   margin-right: 1rem; | ||||
| } | ||||
| @ -808,30 +811,6 @@ select { | ||||
|   margin-top: 1rem; | ||||
| } | ||||
|  | ||||
| .mt-1 { | ||||
|   margin-top: 0.25rem; | ||||
| } | ||||
|  | ||||
| .mt-2 { | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
|  | ||||
| .mb-2 { | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
|  | ||||
| .mb-1 { | ||||
|   margin-bottom: 0.25rem; | ||||
| } | ||||
|  | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
|  | ||||
| .ml-2 { | ||||
|   margin-left: 0.5rem; | ||||
| } | ||||
|  | ||||
| .block { | ||||
|   display: block; | ||||
| } | ||||
| @ -848,10 +827,6 @@ select { | ||||
|   display: table; | ||||
| } | ||||
|  | ||||
| .list-item { | ||||
|   display: list-item; | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   display: none; | ||||
| } | ||||
| @ -872,30 +847,18 @@ select { | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .w-5 { | ||||
|   width: 1.25rem; | ||||
| } | ||||
|  | ||||
| .max-w-screen-lg { | ||||
|   max-width: 1024px; | ||||
| } | ||||
|  | ||||
| .max-w-xs { | ||||
|   max-width: 20rem; | ||||
| } | ||||
|  | ||||
| .max-w-lg { | ||||
|   max-width: 32rem; | ||||
| } | ||||
|  | ||||
| .max-w-3xl { | ||||
|   max-width: 48rem; | ||||
| } | ||||
|  | ||||
| .max-w-sm { | ||||
|   max-width: 24rem; | ||||
| } | ||||
|  | ||||
| .max-w-xs { | ||||
|   max-width: 20rem; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
| @ -906,10 +869,6 @@ select { | ||||
|   animation: spin 1s linear infinite; | ||||
| } | ||||
|  | ||||
| .list-disc { | ||||
|   list-style-type: disc; | ||||
| } | ||||
|  | ||||
| .flex-col { | ||||
|   flex-direction: column; | ||||
| } | ||||
| @ -953,16 +912,6 @@ select { | ||||
|   border-color: rgb(229 231 235 / var(--tw-border-opacity)); | ||||
| } | ||||
|  | ||||
| .border-red-300 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(252 165 165 / var(--tw-border-opacity)); | ||||
| } | ||||
|  | ||||
| .border-red-500 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(239 68 68 / var(--tw-border-opacity)); | ||||
| } | ||||
|  | ||||
| .border-slate-500 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(100 116 139 / var(--tw-border-opacity)); | ||||
| @ -978,11 +927,6 @@ select { | ||||
|   background-color: rgb(255 255 255 / var(--tw-bg-opacity)); | ||||
| } | ||||
|  | ||||
| .bg-slate-400 { | ||||
|   --tw-bg-opacity: 1; | ||||
|   background-color: rgb(148 163 184 / var(--tw-bg-opacity)); | ||||
| } | ||||
|  | ||||
| .p-4 { | ||||
|   padding: 1rem; | ||||
| } | ||||
| @ -1018,12 +962,9 @@ select { | ||||
|   font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | ||||
| } | ||||
|  | ||||
| .font-serif { | ||||
|   font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; | ||||
| } | ||||
|  | ||||
| .font-sans { | ||||
|   font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | ||||
| .text-3xl { | ||||
|   font-size: 1.875rem; | ||||
|   line-height: 2.25rem; | ||||
| } | ||||
|  | ||||
| .text-4xl { | ||||
| @ -1036,6 +977,11 @@ select { | ||||
|   line-height: 1.5rem; | ||||
| } | ||||
|  | ||||
| .text-lg { | ||||
|   font-size: 1.125rem; | ||||
|   line-height: 1.75rem; | ||||
| } | ||||
|  | ||||
| .text-xl { | ||||
|   font-size: 1.25rem; | ||||
|   line-height: 1.75rem; | ||||
| @ -1046,62 +992,10 @@ select { | ||||
|   line-height: 1rem; | ||||
| } | ||||
|  | ||||
| .text-2xl { | ||||
|   font-size: 1.5rem; | ||||
|   line-height: 2rem; | ||||
| } | ||||
|  | ||||
| .text-9xl { | ||||
|   font-size: 8rem; | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .text-8xl { | ||||
|   font-size: 6rem; | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .text-7xl { | ||||
|   font-size: 4.5rem; | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .text-6xl { | ||||
|   font-size: 3.75rem; | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .text-5xl { | ||||
|   font-size: 3rem; | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| .text-lg { | ||||
|   font-size: 1.125rem; | ||||
|   line-height: 1.75rem; | ||||
| } | ||||
|  | ||||
| .text-3xl { | ||||
|   font-size: 1.875rem; | ||||
|   line-height: 2.25rem; | ||||
| } | ||||
|  | ||||
| .font-semibold { | ||||
|   font-weight: 600; | ||||
| } | ||||
|  | ||||
| .font-bold { | ||||
|   font-weight: 700; | ||||
| } | ||||
|  | ||||
| .capitalize { | ||||
|   text-transform: capitalize; | ||||
| } | ||||
|  | ||||
| .italic { | ||||
|   font-style: italic; | ||||
| } | ||||
|  | ||||
| .text-slate-300 { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(203 213 225 / var(--tw-text-opacity)); | ||||
| @ -1117,23 +1011,10 @@ select { | ||||
|   color: rgb(253 224 71 / var(--tw-text-opacity)); | ||||
| } | ||||
|  | ||||
| .text-gray-600 { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(75 85 99 / var(--tw-text-opacity)); | ||||
| } | ||||
|  | ||||
| .underline { | ||||
|   text-decoration-line: underline; | ||||
| } | ||||
|  | ||||
| .decoration-red-500 { | ||||
|   text-decoration-color: #ef4444; | ||||
| } | ||||
|  | ||||
| .decoration-slate-400 { | ||||
|   text-decoration-color: #94a3b8; | ||||
| } | ||||
|  | ||||
| .decoration-slate-500 { | ||||
|   text-decoration-color: #64748b; | ||||
| } | ||||
| @ -1403,11 +1284,17 @@ th label { | ||||
|   color: rgb(255 255 255 / var(--tw-text-opacity)); | ||||
| } | ||||
|  | ||||
| @media (min-width: 640px) { | ||||
|   .sm\:ml-2 { | ||||
|     margin-left: 0.5rem; | ||||
|   } | ||||
| :is(.dark .dark\:text-slate-400) { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(148 163 184 / var(--tw-text-opacity)); | ||||
| } | ||||
|  | ||||
| :is(.dark .dark\:text-slate-500) { | ||||
|   --tw-text-opacity: 1; | ||||
|   color: rgb(100 116 139 / var(--tw-text-opacity)); | ||||
| } | ||||
|  | ||||
| @media (min-width: 640px) { | ||||
|   .sm\:inline { | ||||
|     display: inline; | ||||
|   } | ||||
| @ -1420,10 +1307,6 @@ th label { | ||||
|     max-width: 28rem; | ||||
|   } | ||||
|  | ||||
|   .sm\:max-w-lg { | ||||
|     max-width: 32rem; | ||||
|   } | ||||
|  | ||||
|   .sm\:max-w-xl { | ||||
|     max-width: 36rem; | ||||
|   } | ||||
| @ -1475,11 +1358,11 @@ th label { | ||||
|     display: table-cell; | ||||
|   } | ||||
|  | ||||
|   .lg\:max-w-lg { | ||||
|     max-width: 32rem; | ||||
|   } | ||||
|  | ||||
|   .lg\:max-w-3xl { | ||||
|     max-width: 48rem; | ||||
|   } | ||||
|  | ||||
|   .lg\:max-w-lg { | ||||
|     max-width: 32rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
| {% block content %} | ||||
|     <div class="text-slate-300 mx-auto max-w-screen-lg text-center"> | ||||
|         {% if session_count > 0 %} | ||||
|             You have played a total of {{ session_count }} sessions for a total of {{ total_duration }}. | ||||
|             You have played a total of {{ session_count }} sessions for a total of {{ total_duration_formatted }}. | ||||
|         {% elif not game_available or not platform_available %} | ||||
|             There are no games in the database. Start by clicking "New Game" and "New Platform". | ||||
|         {% elif not purchase_available %} | ||||
| @ -14,4 +14,4 @@ | ||||
|             You haven't played any games yet. Click "New Session" to add one now. | ||||
|         {% endif %} | ||||
|     </div> | ||||
| {% endblock content %} | ||||
| {% endblock content %} | ||||
|  | ||||
| @ -6,10 +6,15 @@ | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> | ||||
|         <h1 class="text-4xl">{{ game.name }} (#{{ game.pk }})</h1> | ||||
|         <h2 class="text-lg my-2 ml-2">{{ total_playtime }} ({{ first_session.timestamp_start | date:"M Y"}} — {{ last_session.timestamp_start | date:"M Y"}}) </h2> | ||||
|         <h1 class="text-4xl">{{ game.name }} <span class="dark:text-slate-500">(#{{ game.pk }})</span></h1> | ||||
|         <h2 class="text-lg my-2 ml-2"> | ||||
|             {{ total_hours }} <span class="dark:text-slate-500">total</span> | ||||
|             {{ session_average }} <span class="dark:text-slate-500">avg</span>  | ||||
|             ({{ first_session.timestamp_start | date:"M Y"}} | ||||
|              —  | ||||
|             {{ last_session.timestamp_start | date:"M Y"}}) </h2> | ||||
|         <hr class="border-slate-500"> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Editions</h1> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Editions <span class="dark:text-slate-500">({{ editions.count }})</span></h1> | ||||
|         <ul> | ||||
|             {% for edition in editions %} | ||||
|             <li class="sm:pl-2"> | ||||
| @ -24,13 +29,13 @@ | ||||
|             </li> | ||||
|             {% endfor %} | ||||
|         </ul> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Purchases</h1> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1> | ||||
|         <ul> | ||||
|             {% for purchase in purchases %} | ||||
|             <li class="sm:pl-2">{{ purchase.platform }} ({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})</li> | ||||
|             {% endfor %} | ||||
|         </ul> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Sessions</h1> | ||||
|         <h1 class="text-3xl mt-4 mb-1">Sessions <span class="dark:text-slate-500">({{ sessions.count }})</span></h1> | ||||
|         <ul> | ||||
|             {% for session in sessions %} | ||||
|             <li class="sm:pl-2">{{ session.timestamp_start | date:"d/m/Y" }} ({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})</li> | ||||
|  | ||||
| @ -2,6 +2,7 @@ from datetime import datetime, timedelta | ||||
| from zoneinfo import ZoneInfo | ||||
|  | ||||
| from common.time import now as now_with_tz | ||||
| from common.time import format_duration | ||||
| from django.conf import settings | ||||
| from django.shortcuts import redirect, render | ||||
|  | ||||
| @ -100,7 +101,12 @@ def view_game(request, game_id=None): | ||||
|     context["sessions"] = Session.objects.filter( | ||||
|         purchase__edition__game_id=game_id | ||||
|     ).order_by("-timestamp_start") | ||||
|     context["total_playtime"] = context["sessions"].total_duration() | ||||
|     context["total_hours"] = int( | ||||
|         format_duration(context["sessions"].total_duration_unformatted(), "%H") | ||||
|     ) | ||||
|     context["session_average"] = round( | ||||
|         (context["total_hours"]) / int(context["sessions"].count()), 1 | ||||
|     ) | ||||
|     # here first and last is flipped | ||||
|     # because sessions are ordered from newest to oldest | ||||
|     # so the most recent are on top | ||||
| @ -197,7 +203,7 @@ def list_sessions( | ||||
|             session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) | ||||
|             session.unfinished = True | ||||
|  | ||||
|     context["total_duration"] = dataset.total_duration() | ||||
|     context["total_duration"] = dataset.total_duration_formatted() | ||||
|     context["dataset"] = dataset | ||||
|     # cannot use dataset[0] here because that might be only partial QuerySet | ||||
|     context["last"] = Session.objects.all().order_by("timestamp_start").last() | ||||
|  | ||||
		Reference in New Issue
	
	Block a user