Compare commits

..

No commits in common. "84dafe922394e6e0a057a0ae10af7faa7cb52aea" and "84c92fe654e8cdd06d58e7f0fbe5702863a38a3c" have entirely different histories.

11 changed files with 78 additions and 182 deletions

View File

@ -1,8 +0,0 @@
## Unreleased
* Hide navigation bar items if there are no games/purchases/sessions
* Set default version to "git-main" to indicate development environment
* Add homepage, link to it from the logo
* Make it possible to add a new platform
* Save calculated duration to database if both timestamps are set
* Improve session listing
* Set version in the footer to fixed, fix main container height

View File

@ -1,5 +1,5 @@
from django import forms
from .models import Session, Purchase, Game, Platform
from .models import Session, Purchase, Game
class SessionForm(forms.ModelForm):
@ -24,9 +24,3 @@ class GameForm(forms.ModelForm):
class Meta:
model = Game
fields = ["name", "wikidata"]
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = ["name", "group"]

View File

@ -38,36 +38,17 @@ class Session(models.Model):
def __str__(self):
mark = ", manual" if self.duration_manual != None else ""
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})"
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
def duration_seconds(self):
def calculated_duration(self):
if self.timestamp_end == None or self.timestamp_start == None:
if self.duration_manual == None:
return 0
else:
value = self.duration_manual
else:
value = self.timestamp_end - self.timestamp_start
return value.total_seconds()
return self.timestamp_end - self.timestamp_start
def duration_formatted(self):
seconds = self.duration_seconds()
if seconds == 0:
return seconds
hours, remainder = divmod(seconds, 3600)
minutes = remainder % 60
hour_string = f"{int(hours)}h" if hours != 0 else ""
minute_string = f"{int(minutes)}m" if minutes != 0 else ""
return f"{hour_string}{minute_string}"
def duration_any(self):
def total_duration(self):
return (
self.duration_formatted()
self.calculated_duration()
if self.duration_manual == None
else self.duration_manual
else self.duration_manual + self.calculated_duration()
)
def save(self, *args, **kwargs):
if self.timestamp_start != None and self.timestamp_end != None:
self.duration_calculated = self.timestamp_end - self.timestamp_start
super(Session, self).save(*args, **kwargs)

View File

@ -717,16 +717,24 @@ select {
position: static;
}
.fixed {
position: fixed;
.absolute {
position: absolute;
}
.left-2 {
left: 0.5rem;
.left-0 {
left: 0px;
}
.bottom-2 {
bottom: 0.5rem;
.bottom-0 {
bottom: 0px;
}
.left-1 {
left: 0.25rem;
}
.bottom-1 {
bottom: 0.25rem;
}
.mx-auto {
@ -754,8 +762,8 @@ select {
display: grid;
}
.min-h-screen {
min-height: 100vh;
.h-screen {
height: 100vh;
}
.w-full {
@ -810,35 +818,16 @@ select {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.border-red-800 {
--tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity));
}
.border-red-900 {
--tw-border-opacity: 1;
border-color: rgb(127 29 29 / var(--tw-border-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-red-700 {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.p-4 {
padding: 1rem;
}
@ -847,10 +836,6 @@ select {
padding: 0.5rem;
}
.p-1 {
padding: 0.25rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -878,11 +863,6 @@ select {
line-height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
@ -893,6 +873,11 @@ select {
line-height: 1.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-semibold {
font-weight: 600;
}
@ -902,6 +887,31 @@ select {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-slate-800 {
--tw-text-opacity: 1;
color: rgb(30 41 59 / var(--tw-text-opacity));
}
.text-slate-700 {
--tw-text-opacity: 1;
color: rgb(51 65 85 / var(--tw-text-opacity));
}
.text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
.text-slate-500 {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
.text-slate-400 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
.text-slate-300 {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity));
@ -934,35 +944,6 @@ form input[type=submit] {
padding: 0.5rem;
}
.hover\:border-dotted:hover {
border-style: dotted;
}
.hover\:border-white:hover {
--tw-border-opacity: 1;
border-color: rgb(255 255 255 / var(--tw-border-opacity));
}
.hover\:bg-red-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.hover\:bg-yellow-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(161 98 7 / var(--tw-bg-opacity));
}
.hover\:bg-orange-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@ -987,16 +968,16 @@ form input[type=submit] {
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
}
.dark .dark\:bg-slate-400 {
--tw-bg-opacity: 1;
background-color: rgb(148 163 184 / var(--tw-bg-opacity));
}
.dark .dark\:text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark .dark\:text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
.dark .dark\:text-slate-400 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
@ -1007,6 +988,11 @@ form input[type=submit] {
color: rgb(203 213 225 / var(--tw-text-opacity));
}
.dark .dark\:text-slate-600 {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
}
@media (min-width: 768px) {
.md\:block {
display: block;

View File

@ -12,10 +12,10 @@
</head>
<body class="dark">
<div class="dark:bg-gray-800 min-h-screen">
<div class="dark:bg-gray-800 h-screen">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
<div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="{% url 'index' %}" class="flex items-center">
<a href="#" class="flex items-center">
<span class="text-4xl"></span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a>
@ -23,16 +23,9 @@
<ul
class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li>
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_platform' %}">New Platform</a></li>
{% if game_available and platform_available %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li>
{% endif %}
{% if purchase_available %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
{% endif %}
{% if session_count > 0 %}
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
{% endif %}
</ul>
</div>
</div>
@ -40,7 +33,7 @@
{% block content %}No content here.{% endblock %}
</div>
{% load version %}
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
<span id="version-info" class="absolute left-1 bottom-1 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</body>
</html>

View File

@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% block title %}{{ title }}{% endblock title %}
{% 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.
{% else %}
Start by clicking the links at the top. To track playtime, you need to have at least 1 owned game.
{% endif %}
</div>
{% endblock content %}

View File

@ -17,17 +17,8 @@
{% for data in dataset %}
<div class=""><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div>
<div class="dark:text-slate-400">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400">
{% if data.unfinished %}
Not finished yet. <button class="bg-red-700 hover:bg-orange-700 border border-red-900 hover:border-dotted hover:border-white rounded p-1 text-white text-sm">Finish now?</button>
{% elif data.duration_manual %}
MANUAL
{% else %}
{{ data.timestamp_end | date:"d/m/Y H:i" }}
{% endif %}
</div>
{% load time %}
<div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div>
<div class="dark:text-slate-400">{{ data.timestamp_end | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400">{{ data.time_delta }}</div>
{% endfor %}
</div>
{% endblock content %}

View File

@ -15,4 +15,4 @@ def version_date():
@register.simple_tag
def version():
return os.environ.get("VERSION_NUMBER", "git-main")
return os.environ.get("VERSION_NUMBER", "UNKNOWN VERSION")

View File

@ -3,9 +3,7 @@ from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("add-game/", views.add_game, name="add_game"),
path("add-platform/", views.add_platform, name="add_platform"),
path("add-session/", views.add_session, name="add_session"),
path("add-purchase/", views.add_purchase, name="add_purchase"),
path("list-sessions/", views.list_sessions, name="list_sessions"),

View File

@ -1,19 +1,10 @@
from django.shortcuts import render
from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm
from .forms import SessionForm, PurchaseForm, GameForm
from datetime import datetime
from zoneinfo import ZoneInfo
from django.conf import settings
def model_counts(request):
return {
"game_available": Game.objects.count() != 0,
"platform_available": Platform.objects.count() != 0,
"purchase_available": Purchase.objects.count() != 0,
"session_count": Session.objects.count(),
}
from django.db.models import ExpressionWrapper, F, DurationField
import logging
def add_session(request):
@ -37,11 +28,11 @@ def list_sessions(request, purchase_id=None):
else:
dataset = Session.objects.all()
for session in dataset:
if session.timestamp_end == None and session.duration_manual == None:
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
session.unfinished = True
dataset = dataset.annotate(
time_delta=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
)
)
context["dataset"] = dataset
return render(request, "list_sessions.html", context)
@ -69,19 +60,3 @@ def add_game(request):
context["form"] = form
context["title"] = "Add New Game"
return render(request, "add.html", context)
def add_platform(request):
context = {}
form = PlatformForm(request.POST or None)
if form.is_valid():
form.save()
context["form"] = form
context["title"] = "Add New Platform"
return render(request, "add.html", context)
def index(request):
context = {}
return render(request, "index.html", context)

View File

@ -65,7 +65,6 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"tracker.views.model_counts",
],
},
},