Compare commits
19 Commits
0.1.0
...
4b45127335
Author | SHA1 | Date | |
---|---|---|---|
4b45127335 | |||
b8a15e43db | |||
a1309c3738 | |||
12cc9025a0 | |||
6fe960bc04 | |||
61d2e65d83
|
|||
84dafe9223
|
|||
59cf620ff3
|
|||
40810256aa
|
|||
b3842504af
|
|||
bf61326c18
|
|||
4c642d97cb
|
|||
d225856174
|
|||
5c50e059e6
|
|||
84c92fe654 | |||
166dd716ed | |||
6102459637 | |||
e4cd75d51f | |||
b1c8f58855 |
@ -8,11 +8,8 @@ steps:
|
|||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
repo: registry.kucharczyk.xyz/timetracker
|
repo: registry.kucharczyk.xyz/timetracker
|
||||||
environment:
|
|
||||||
VERSION_NUMBER: $(git describe --tags --abbrev=0)
|
|
||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- $VERSION_NUMBER
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
|
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## Unreleased
|
||||||
|
* Allow deleting sessions
|
||||||
|
* Redirect after adding game/platform/purchase/session
|
||||||
|
* Fix display of duration_manual
|
||||||
|
* Fix display of duration_calculated, display durations less than a minute
|
||||||
|
* Make the "Finish now?" button on session list work
|
||||||
|
* 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
|
@ -12,4 +12,5 @@ COPY entrypoint.sh /
|
|||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
USER timetracker
|
USER timetracker
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
ENV VERSION_NUMBER 0.1.0-18-gb8a15e4
|
||||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
ENTRYPOINT [ "/entrypoint.sh" ]
|
7
Makefile
7
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: createsuperuser
|
.PHONY: createsuperuser shell
|
||||||
|
|
||||||
all: css migrate
|
all: css migrate
|
||||||
|
|
||||||
@ -34,4 +34,7 @@ loadsample:
|
|||||||
python src/web/manage.py loaddata sample.yaml
|
python src/web/manage.py loaddata sample.yaml
|
||||||
|
|
||||||
createsuperuser:
|
createsuperuser:
|
||||||
python src/web/manage.py createsuperuser
|
python src/web/manage.py createsuperuser
|
||||||
|
|
||||||
|
shell:
|
||||||
|
python src/web/manage.py shell
|
0
src/web/common/util/__init__.py
Normal file
0
src/web/common/util/__init__.py
Normal file
7
src/web/common/util/time.py
Normal file
7
src/web/common/util/time.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
|
def now():
|
||||||
|
return datetime.now(ZoneInfo(settings.TIME_ZONE))
|
@ -1,5 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Session, Purchase, Game
|
from .models import Session, Purchase, Game, Platform
|
||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.ModelForm):
|
class SessionForm(forms.ModelForm):
|
||||||
@ -24,3 +24,9 @@ class GameForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Game
|
model = Game
|
||||||
fields = ["name", "wikidata"]
|
fields = ["name", "wikidata"]
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Platform
|
||||||
|
fields = ["name", "group"]
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from datetime import timedelta
|
from datetime import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
class Game(models.Model):
|
class Game(models.Model):
|
||||||
@ -38,17 +40,42 @@ class Session(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
mark = ", manual" if self.duration_manual != None else ""
|
mark = ", manual" if self.duration_manual != None else ""
|
||||||
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
|
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_any()}{mark})"
|
||||||
|
|
||||||
def calculated_duration(self):
|
def finish_now(self):
|
||||||
if self.timestamp_end == None or self.timestamp_start == None:
|
self.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
|
||||||
return 0
|
|
||||||
|
def duration_seconds(self):
|
||||||
|
if self.duration_manual == None:
|
||||||
|
if self.timestamp_end == None or self.timestamp_start == None:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
value = self.timestamp_end - self.timestamp_start
|
||||||
else:
|
else:
|
||||||
return self.timestamp_end - self.timestamp_start
|
value = self.duration_manual
|
||||||
|
return value.total_seconds()
|
||||||
|
|
||||||
def total_duration(self):
|
def duration_formatted(self):
|
||||||
|
seconds = self.duration_seconds()
|
||||||
|
if seconds == 0:
|
||||||
|
return seconds
|
||||||
|
hours, remainder = divmod(seconds, 3600)
|
||||||
|
minutes = remainder // 60
|
||||||
|
if hours == 0 and minutes == 0:
|
||||||
|
return "less than a minute"
|
||||||
|
else:
|
||||||
|
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):
|
||||||
return (
|
return (
|
||||||
self.calculated_duration()
|
self.duration_formatted()
|
||||||
if self.duration_manual == None
|
if self.duration_manual == None
|
||||||
else self.duration_manual + self.calculated_duration()
|
else self.duration_manual
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -717,24 +717,16 @@ select {
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.absolute {
|
.fixed {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-0 {
|
.left-2 {
|
||||||
left: 0px;
|
left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-0 {
|
.bottom-2 {
|
||||||
bottom: 0px;
|
bottom: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.left-1 {
|
|
||||||
left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-1 {
|
|
||||||
bottom: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
@ -762,8 +754,8 @@ select {
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-screen {
|
.min-h-screen {
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-full {
|
.w-full {
|
||||||
@ -774,8 +766,8 @@ select {
|
|||||||
max-width: 1024px;
|
max-width: 1024px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cols-4 {
|
.grid-cols-5 {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
@ -818,16 +810,30 @@ select {
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.border-gray-200 {
|
.border-gray-200 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-red-900 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(127 29 29 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-white {
|
.bg-white {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
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 {
|
.p-4 {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@ -836,6 +842,10 @@ select {
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-1 {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.py-2 {
|
.py-2 {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
@ -863,6 +873,11 @@ select {
|
|||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.text-lg {
|
.text-lg {
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
@ -873,11 +888,6 @@ select {
|
|||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@ -887,31 +897,6 @@ select {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
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 {
|
.text-slate-300 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
color: rgb(203 213 225 / var(--tw-text-opacity));
|
||||||
@ -944,6 +929,20 @@ form input[type=submit] {
|
|||||||
padding: 0.5rem;
|
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-orange-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(194 65 12 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:underline:hover {
|
.hover\:underline:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
@ -968,16 +967,16 @@ form input[type=submit] {
|
|||||||
background-color: rgb(51 65 85 / var(--tw-bg-opacity));
|
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 {
|
.dark .dark\:text-white {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
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 {
|
.dark .dark\:text-slate-400 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||||
@ -988,11 +987,6 @@ form input[type=submit] {
|
|||||||
color: rgb(203 213 225 / var(--tw-text-opacity));
|
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) {
|
@media (min-width: 768px) {
|
||||||
.md\:block {
|
.md\:block {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dark">
|
<body class="dark">
|
||||||
<div class="dark:bg-gray-800 h-screen">
|
<div class="dark:bg-gray-800 min-h-screen">
|
||||||
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
<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">
|
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
||||||
<a href="#" class="flex items-center">
|
<a href="{% url 'index' %}" class="flex items-center">
|
||||||
<span class="text-4xl">⌚</span>
|
<span class="text-4xl">⌚</span>
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
||||||
</a>
|
</a>
|
||||||
@ -23,9 +23,16 @@
|
|||||||
<ul
|
<ul
|
||||||
class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
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_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>
|
<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>
|
<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>
|
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -33,7 +40,7 @@
|
|||||||
{% block content %}No content here.{% endblock %}
|
{% block content %}No content here.{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% load version %}
|
{% load version %}
|
||||||
<span id="version-info" class="absolute left-1 bottom-1 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
13
src/web/tracker/templates/index.html
Normal file
13
src/web/tracker/templates/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% 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 %}
|
@ -9,16 +9,28 @@
|
|||||||
<a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a>
|
<a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="grid grid-cols-4 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
<div class="grid grid-cols-5 gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Start</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Start</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">End</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">End</div>
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
||||||
|
<div></div>
|
||||||
{% for data in dataset %}
|
{% 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=""><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">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
||||||
<div class="dark:text-slate-400">{{ data.timestamp_end | date:"d/m/Y H:i" }}</div>
|
<div class="dark:text-slate-400">
|
||||||
<div class="dark:text-slate-400">{{ data.time_delta }}</div>
|
{% if data.unfinished %}
|
||||||
|
Not finished yet. <a href="{% url 'update_session' data.id %}"><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></a>
|
||||||
|
{% elif data.duration_manual %}
|
||||||
|
MANUAL
|
||||||
|
{% else %}
|
||||||
|
{{ data.timestamp_end | date:"d/m/Y H:i" }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="dark:text-slate-400">{{ data.duration_formatted }}{% if data.duration_manual %} (M){% endif %}</div>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'delete_session' data.id %}"><button>❌</button></a>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -15,4 +15,4 @@ def version_date():
|
|||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def version():
|
def version():
|
||||||
return os.environ.get("VERSION_NUMBER", "UNKNOWN VERSION")
|
return os.environ.get("VERSION_NUMBER", "git-main")
|
||||||
|
@ -3,8 +3,20 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("", views.index, name="index"),
|
||||||
path("add-game/", views.add_game, name="add_game"),
|
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-session/", views.add_session, name="add_session"),
|
||||||
|
path(
|
||||||
|
"update-session/by-session/<int:session_id>",
|
||||||
|
views.update_session,
|
||||||
|
name="update_session",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"delete_session/by-id/<int:session_id>",
|
||||||
|
views.delete_session,
|
||||||
|
name="delete_session",
|
||||||
|
),
|
||||||
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||||
path("list-sessions/", views.list_sessions, name="list_sessions"),
|
path("list-sessions/", views.list_sessions, name="list_sessions"),
|
||||||
path(
|
path(
|
||||||
|
@ -1,24 +1,48 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render, redirect
|
||||||
|
|
||||||
from .models import Game, Platform, Purchase, Session
|
from .models import Game, Platform, Purchase, Session
|
||||||
from .forms import SessionForm, PurchaseForm, GameForm
|
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.db.models import ExpressionWrapper, F, DurationField
|
from zoneinfo import ZoneInfo
|
||||||
import logging
|
from django.conf import settings
|
||||||
|
from common.util.time import now as now_with_tz
|
||||||
|
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def add_session(request):
|
def add_session(request):
|
||||||
context = {}
|
context = {}
|
||||||
now = datetime.now()
|
now = now_with_tz()
|
||||||
initial = {"timestamp_start": now, "timestamp_end": now}
|
initial = {"timestamp_start": now}
|
||||||
form = SessionForm(request.POST or None, initial=initial)
|
form = SessionForm(request.POST or None, initial=initial)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
|
return redirect("list_sessions")
|
||||||
|
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
return render(request, "add_session.html", context)
|
return render(request, "add_session.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def update_session(request, session_id=None):
|
||||||
|
session = Session.objects.get(id=session_id)
|
||||||
|
session.finish_now()
|
||||||
|
session.save()
|
||||||
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_session(request, session_id=None):
|
||||||
|
session = Session.objects.get(id=session_id)
|
||||||
|
session.delete()
|
||||||
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
def list_sessions(request, purchase_id=None):
|
def list_sessions(request, purchase_id=None):
|
||||||
context = {}
|
context = {}
|
||||||
|
|
||||||
@ -28,11 +52,11 @@ def list_sessions(request, purchase_id=None):
|
|||||||
else:
|
else:
|
||||||
dataset = Session.objects.all()
|
dataset = Session.objects.all()
|
||||||
|
|
||||||
dataset = dataset.annotate(
|
for session in dataset:
|
||||||
time_delta=ExpressionWrapper(
|
if session.timestamp_end == None and session.duration_manual == None:
|
||||||
F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
|
session.timestamp_end = datetime.now(ZoneInfo(settings.TIME_ZONE))
|
||||||
)
|
session.unfinished = True
|
||||||
)
|
|
||||||
context["dataset"] = dataset
|
context["dataset"] = dataset
|
||||||
|
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
@ -45,9 +69,11 @@ def add_purchase(request):
|
|||||||
form = PurchaseForm(request.POST or None, initial=initial)
|
form = PurchaseForm(request.POST or None, initial=initial)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
|
return redirect("index")
|
||||||
|
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
return render(request, "add_purchase.html", context)
|
context["title"] = "Add New Purchase"
|
||||||
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_game(request):
|
def add_game(request):
|
||||||
@ -55,7 +81,25 @@ def add_game(request):
|
|||||||
form = GameForm(request.POST or None)
|
form = GameForm(request.POST or None)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
|
return redirect("index")
|
||||||
|
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["title"] = "Add New Game"
|
context["title"] = "Add New Game"
|
||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def add_platform(request):
|
||||||
|
context = {}
|
||||||
|
form = PlatformForm(request.POST or None)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect("index")
|
||||||
|
|
||||||
|
context["form"] = form
|
||||||
|
context["title"] = "Add New Platform"
|
||||||
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
context = {}
|
||||||
|
return render(request, "index.html", context)
|
||||||
|
@ -65,6 +65,7 @@ TEMPLATES = [
|
|||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
"tracker.views.model_counts",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user