7 Commits

Author SHA1 Message Date
dba8414fd9 Version 1.1.2
Some checks failed
continuous-integration/drone/push Build is failing
2023-10-13 16:33:55 +02:00
0e2113eefd Display durations in a consistent manner
Fixes #61
2023-10-13 16:32:12 +02:00
c4b0347f3b Version 1.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-09 20:56:23 +02:00
c6ed21167c Remove debugging cruft from container 2023-10-09 20:56:13 +02:00
4ce15c44fc Add edit buttons to game overview, notes 2023-10-09 20:55:31 +02:00
c814b4c2cb Version 1.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-09 00:04:46 +02:00
11b9c602de Improve game overview
- add counts for each section
- add average hours per session
2023-10-09 00:00:45 +02:00
11 changed files with 226 additions and 179 deletions

View File

@ -18,6 +18,7 @@ steps:
repo: registry.kucharczyk.xyz/timetracker repo: registry.kucharczyk.xyz/timetracker
tags: tags:
- latest - latest
- 1.1.0
depends_on: depends_on:
- "test" - "test"
when: when:

View File

@ -1,4 +1,20 @@
## Unreleased ## 1.1.2 / 2023-10-13 16:30+02:00
### Enhancements
* Durations are formatted in a consisent manner across all pages
### Fixes
* Game Overview: display duration when >1 hour instead of displaying 0
## 1.1.1 / 2023-10-09 20:52+02:00
### New
* Add notes section to game overview
### Enhancements
* Make it possible to add any data on the game overview page
## 1.1.0 / 2023-10-09 00:01+02:00
### New ### New
* Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8) * Add game overview page (https://git.kucharczyk.xyz/lukas/timetracker/issues/8)

View File

@ -6,17 +6,10 @@ RUN npm install && \
FROM python:3.10.9-slim-bullseye FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 1.0.3 ENV VERSION_NUMBER 1.1.2
ENV PROD 1 ENV PROD 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
RUN apt update && \
apt install -y \
bash \
vim \
curl && \
rm -rf /var/lib/apt/lists/*
RUN useradd -m --uid 1000 timetracker RUN useradd -m --uid 1000 timetracker
WORKDIR /home/timetracker/app WORKDIR /home/timetracker/app
COPY . /home/timetracker/app/ COPY . /home/timetracker/app/

View File

@ -32,13 +32,15 @@ def format_duration(
from the formatting string. For example: from the formatting string. For example:
- 61 seconds as "%s" = 61 seconds - 61 seconds as "%s" = 61 seconds
- 61 seconds as "%m %s" = 1 minutes 1 seconds" - 61 seconds as "%m %s" = 1 minutes 1 seconds"
Format specifiers can include width and precision options:
- %5.2H: hours formatted with width 5 and 2 decimal places (padded with zeros)
""" """
minute_seconds = 60 minute_seconds = 60
hour_seconds = 60 * minute_seconds hour_seconds = 60 * minute_seconds
day_seconds = 24 * hour_seconds day_seconds = 24 * hour_seconds
duration = _safe_timedelta(duration) safe_duration = _safe_timedelta(duration)
# we don't need float # we don't need float
seconds_total = int(duration.total_seconds()) seconds_total = int(safe_duration.total_seconds())
# timestamps where end is before start # timestamps where end is before start
if seconds_total < 0: if seconds_total < 0:
seconds_total = 0 seconds_total = 0
@ -46,18 +48,29 @@ def format_duration(
remainder = seconds = seconds_total remainder = seconds = seconds_total
if "%d" in format_string: if "%d" in format_string:
days, remainder = divmod(seconds_total, day_seconds) days, remainder = divmod(seconds_total, day_seconds)
if "%H" in format_string: if re.search(r"%\d*\.?\d*H", format_string):
hours, remainder = divmod(remainder, hour_seconds) hours_float, remainder = divmod(remainder, hour_seconds)
if "%m" in format_string: hours = float(hours_float) + remainder / hour_seconds
if re.search(r"%\d*\.?\d*m", format_string):
minutes, seconds = divmod(remainder, minute_seconds) minutes, seconds = divmod(remainder, minute_seconds)
literals = { literals = {
"%d": str(days), "d": str(days),
"%H": str(hours), "H": str(hours),
"%m": str(minutes), "m": str(minutes),
"%s": str(seconds), "s": str(seconds),
"%r": str(seconds_total), "r": str(seconds_total),
} }
formatted_string = format_string formatted_string = format_string
for pattern, replacement in literals.items(): for pattern, replacement in literals.items():
formatted_string = re.sub(pattern, replacement, formatted_string) # Match format specifiers with optional width and precision
match = re.search(rf"%(\d*\.?\d*){pattern}", formatted_string)
if match:
format_spec = match.group(1)
if format_spec:
# Format the number according to the specifier
replacement = f"{float(replacement):{format_spec}f}"
# Replace the format specifier with the formatted number
formatted_string = re.sub(
rf"%\d*\.?\d*{pattern}", replacement, formatted_string
)
return formatted_string return formatted_string

View File

@ -73,11 +73,14 @@ class Platform(models.Model):
class SessionQuerySet(models.QuerySet): 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( result = self.aggregate(
duration=Sum(F("duration_calculated") + F("duration_manual")) duration=Sum(F("duration_calculated") + F("duration_manual"))
) )
return format_duration(result["duration"]) return result["duration"]
class Session(models.Model): class Session(models.Model):
@ -111,12 +114,12 @@ class Session(models.Model):
return timedelta(seconds=(manual + calculated).total_seconds()) return timedelta(seconds=(manual + calculated).total_seconds())
def duration_formatted(self) -> str: def duration_formatted(self) -> str:
result = format_duration(self.duration_seconds(), "%H:%m") result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
return result return result
@property @property
def duration_sum(self) -> str: def duration_sum(self) -> str:
return Session.objects.all().total_duration() return Session.objects.all().total_duration_formatted()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.timestamp_start != None and self.timestamp_end != None: if self.timestamp_start != None and self.timestamp_end != None:

View File

@ -771,24 +771,14 @@ select {
top: 0.75rem; top: 0.75rem;
} }
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mx-2 { .mx-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.my-1 { .mx-auto {
margin-top: 0.25rem; margin-left: auto;
margin-bottom: 0.25rem; margin-right: auto;
} }
.my-2 { .my-2 {
@ -796,10 +786,23 @@ select {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-4 { .mb-4 {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.ml-2 {
margin-left: 0.5rem;
}
.mr-4 { .mr-4 {
margin-right: 1rem; margin-right: 1rem;
} }
@ -808,30 +811,14 @@ select {
margin-top: 1rem; margin-top: 1rem;
} }
.mt-1 { .ml-8 {
margin-top: 0.25rem; margin-left: 2rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
} }
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
.ml-2 {
margin-left: 0.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -848,10 +835,6 @@ select {
display: table; display: table;
} }
.list-item {
display: list-item;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -860,6 +843,14 @@ select {
height: 1.5rem; height: 1.5rem;
} }
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
@ -876,26 +867,22 @@ select {
width: 1.25rem; width: 1.25rem;
} }
.w-7 {
width: 1.75rem;
}
.max-w-screen-lg { .max-w-screen-lg {
max-width: 1024px; 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-w-sm {
max-width: 24rem; max-width: 24rem;
} }
.max-w-xs {
max-width: 20rem;
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@ -906,10 +893,6 @@ select {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.list-disc {
list-style-type: disc;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@ -922,6 +905,10 @@ select {
align-items: center; align-items: center;
} }
.justify-center {
justify-content: center;
}
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
@ -953,16 +940,6 @@ select {
border-color: rgb(229 231 235 / var(--tw-border-opacity)); 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 { .border-slate-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity)); border-color: rgb(100 116 139 / var(--tw-border-opacity));
@ -978,9 +955,14 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-slate-400 { .bg-blue-600 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity)); background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
.bg-violet-600 {
--tw-bg-opacity: 1;
background-color: rgb(124 58 237 / var(--tw-bg-opacity));
} }
.p-4 { .p-4 {
@ -1010,6 +992,10 @@ select {
padding-right: 1rem; padding-right: 1rem;
} }
.pl-8 {
padding-left: 2rem;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1018,12 +1004,9 @@ select {
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
} }
.font-serif { .text-3xl {
font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; font-size: 1.875rem;
} line-height: 2.25rem;
.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-4xl { .text-4xl {
@ -1036,6 +1019,11 @@ select {
line-height: 1.5rem; line-height: 1.5rem;
} }
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-xl { .text-xl {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
@ -1046,58 +1034,10 @@ select {
line-height: 1rem; 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-semibold {
font-weight: 600; font-weight: 600;
} }
.font-bold {
font-weight: 700;
}
.capitalize {
text-transform: capitalize;
}
.italic { .italic {
font-style: italic; font-style: italic;
} }
@ -1117,23 +1057,10 @@ select {
color: rgb(253 224 71 / var(--tw-text-opacity)); 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 { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }
.decoration-red-500 {
text-decoration-color: #ef4444;
}
.decoration-slate-400 {
text-decoration-color: #94a3b8;
}
.decoration-slate-500 { .decoration-slate-500 {
text-decoration-color: #64748b; text-decoration-color: #64748b;
} }
@ -1355,6 +1282,16 @@ th label {
background-color: rgb(21 128 61 / var(--tw-bg-opacity)); background-color: rgb(21 128 61 / var(--tw-bg-opacity));
} }
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.hover\:bg-violet-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(109 40 217 / var(--tw-bg-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@ -1375,6 +1312,16 @@ th label {
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity)); --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
} }
.focus\:ring-blue-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.focus\:ring-violet-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity));
}
.focus\:ring-offset-2:focus { .focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
@ -1383,6 +1330,10 @@ th label {
--tw-ring-offset-color: #bfdbfe; --tw-ring-offset-color: #bfdbfe;
} }
.focus\:ring-offset-violet-200:focus {
--tw-ring-offset-color: #ddd6fe;
}
:is(.dark .dark\:bg-gray-800) { :is(.dark .dark\:bg-gray-800) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@ -1393,6 +1344,11 @@ th label {
background-color: rgb(17 24 39 / var(--tw-bg-opacity)); background-color: rgb(17 24 39 / var(--tw-bg-opacity));
} }
:is(.dark .dark\:text-slate-500) {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
:is(.dark .dark\:text-slate-600) { :is(.dark .dark\:text-slate-600) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity)); color: rgb(71 85 105 / var(--tw-text-opacity));
@ -1404,10 +1360,6 @@ th label {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:ml-2 {
margin-left: 0.5rem;
}
.sm\:inline { .sm\:inline {
display: inline; display: inline;
} }
@ -1420,10 +1372,6 @@ th label {
max-width: 28rem; max-width: 28rem;
} }
.sm\:max-w-lg {
max-width: 32rem;
}
.sm\:max-w-xl { .sm\:max-w-xl {
max-width: 36rem; max-width: 36rem;
} }
@ -1437,6 +1385,10 @@ th label {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.sm\:pl-4 {
padding-left: 1rem;
}
.sm\:decoration-2 { .sm\:decoration-2 {
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
} }
@ -1475,11 +1427,11 @@ th label {
display: table-cell; display: table-cell;
} }
.lg\:max-w-lg {
max-width: 32rem;
}
.lg\:max-w-3xl { .lg\:max-w-3xl {
max-width: 48rem; max-width: 48rem;
} }
.lg\:max-w-lg {
max-width: 32rem;
}
} }

View File

@ -0,0 +1,21 @@
<a href="{{ edit_url }}">
<button
type="button"
title="Edit"
class="ml-1 py-1 px-2 flex justify-center items-center bg-violet-600 hover:bg-violet-700 focus:ring-violet-500 focus:ring-offset-violet-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
/>
<path
d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"
/>
</svg>
</button>
</a>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="text-slate-300 mx-auto max-w-screen-lg text-center"> <div class="text-slate-300 mx-auto max-w-screen-lg text-center">
{% if session_count > 0 %} {% 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 %} {% elif not game_available or not platform_available %}
There are no games in the database. Start by clicking "New Game" and "New Platform". There are no games in the database. Start by clicking "New Game" and "New Platform".
{% elif not purchase_available %} {% elif not purchase_available %}
@ -14,4 +14,4 @@
You haven't played any games yet. Click "New Session" to add one now. You haven't played any games yet. Click "New Session" to add one now.
{% endif %} {% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -6,13 +6,23 @@
{% block content %} {% block content %}
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> <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> <h1 class="text-4xl flex items-center">
<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> {{ game.name }}
<span class="dark:text-slate-500">(#{{ game.pk }})</span>
{% url 'edit_game' game.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %}
</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"> <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> <ul>
{% for edition in editions %} {% for edition in editions %}
<li class="sm:pl-2"> <li class="sm:pl-2 flex items-center">
{{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }}) {{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }})
{% if edition.wikidata %} {% if edition.wikidata %}
<span class="hidden sm:inline"> <span class="hidden sm:inline">
@ -21,19 +31,50 @@
</a> </a>
</span> </span>
{% endif %} {% endif %}
{% url 'edit_edition' edition.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </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> <ul>
{% for purchase in purchases %} {% 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> <li class="sm:pl-2 flex items-center">
{{ purchase.platform }}
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
{% url 'edit_purchase' purchase.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %}
</li>
{% endfor %} {% endfor %}
</ul> </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> <ul>
{% for session in sessions %} {% 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> <li class="sm:pl-2 flex items-center">
{{ session.timestamp_start | date:"d/m/Y" }}
({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})
{% url 'edit_session' session.id as edit_url %}
{% include 'components/edit_button.html' with edit_url=edit_url %}
</li>
{% endfor %}
</ul>
<h1 class="text-3xl mt-4 mb-1">Notes <span class="dark:text-slate-500">({{ sessions_with_notes.count }})</span></h1>
<ul>
{% for session in sessions_with_notes %}
<li class="sm:pl-2">
<ul>
<li class="block dark:text-slate-500">
<span class="flex items-center">
{{ session.timestamp_start | date:"d/m/Y H:m" }}
{% url 'edit_session' session.id as edit_session_url %}
{% include 'components/edit_button.html' with edit_url=edit_session_url %}
</span>
</li>
<li class="sm:pl-4 italic">
{{ session.note|linebreaks }}
</li>
</ul>
</li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from common.time import now as now_with_tz from common.time import now as now_with_tz
from common.time import format_duration
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
@ -100,12 +101,18 @@ def view_game(request, game_id=None):
context["sessions"] = Session.objects.filter( context["sessions"] = Session.objects.filter(
purchase__edition__game_id=game_id purchase__edition__game_id=game_id
).order_by("-timestamp_start") ).order_by("-timestamp_start")
context["total_playtime"] = context["sessions"].total_duration() context["total_hours"] = float(
format_duration(context["sessions"].total_duration_unformatted(), "%2.1H")
)
context["session_average"] = round(
(context["total_hours"]) / int(context["sessions"].count()), 1
)
# here first and last is flipped # here first and last is flipped
# because sessions are ordered from newest to oldest # because sessions are ordered from newest to oldest
# so the most recent are on top # so the most recent are on top
context["last_session"] = context["sessions"].first() context["last_session"] = context["sessions"].first()
context["first_session"] = context["sessions"].last() context["first_session"] = context["sessions"].last()
context["sessions_with_notes"] = context["sessions"].exclude(note="")
return render(request, "view_game.html", context) return render(request, "view_game.html", context)
@ -197,7 +204,7 @@ def list_sessions(
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE)) session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
session.unfinished = True session.unfinished = True
context["total_duration"] = dataset.total_duration() context["total_duration"] = dataset.total_duration_formatted()
context["dataset"] = dataset context["dataset"] = dataset
# cannot use dataset[0] here because that might be only partial QuerySet # cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.all().order_by("timestamp_start").last() context["last"] = Session.objects.all().order_by("timestamp_start").last()

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "timetracker" name = "timetracker"
version = "1.0.3" version = "1.1.2"
description = "A simple time tracker." description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"] authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
license = "GPL" license = "GPL"