10 Commits

Author SHA1 Message Date
250f841e00 Try fixing CI
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 21:44:08 +01:00
89adf479f6 Include version in the footer
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 21:35:09 +01:00
5f9ca5781f Properly set TIME_ZONE 2023-01-03 20:52:23 +01:00
1a2f0b974d Set timezone from TZ env variable
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 20:41:26 +01:00
c6bb60bbbb Prevent error from empty timestamps
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 20:23:49 +01:00
126e758172 Add trusted domain for CSRF
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 20:11:59 +01:00
6e4db38ee4 Change session datetime display format
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 19:03:46 +01:00
aae05f23e7 Filtering sessions by purchase 2023-01-03 19:03:30 +01:00
cd35af471a Enable logging 2023-01-03 19:01:17 +01:00
d896a37779 Fix dark mode issues 2023-01-03 19:01:10 +01:00
11 changed files with 212 additions and 31 deletions

View File

@ -8,7 +8,11 @@ steps:
image: plugins/docker image: plugins/docker
settings: settings:
repo: registry.kucharczyk.xyz/timetracker repo: registry.kucharczyk.xyz/timetracker
tags: latest environment:
VERSION_NUMBER: $(git describe --tags --abbrev=0)
tags:
- latest
- $VERSION_NUMBER
trigger: trigger:
event: event:
- push - push

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "timetracker" name = "timetracker"
version = "0.1.0" version = "0.0.0"
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"

View File

@ -1,3 +1,15 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
form label {
@apply dark:text-slate-400;
}
form input,select,textarea {
@apply dark:bg-slate-500 dark:border dark:border-slate-900 dark:text-slate-100;
}
form input[type=submit] {
@apply p-2 bg-purple-900;
}

View File

@ -41,6 +41,9 @@ class Session(models.Model):
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.total_duration()}{mark})"
def calculated_duration(self): def calculated_duration(self):
if self.timestamp_end == None or self.timestamp_start == None:
return 0
else:
return self.timestamp_end - self.timestamp_start return self.timestamp_end - self.timestamp_start
def total_duration(self): def total_duration(self):

View File

@ -717,6 +717,26 @@ select {
position: static; position: static;
} }
.absolute {
position: absolute;
}
.left-0 {
left: 0px;
}
.bottom-0 {
bottom: 0px;
}
.left-1 {
left: 0.25rem;
}
.bottom-1 {
bottom: 0.25rem;
}
.mx-auto { .mx-auto {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@ -742,6 +762,10 @@ select {
display: grid; display: grid;
} }
.h-screen {
height: 100vh;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -825,6 +849,10 @@ select {
padding-right: 1rem; padding-right: 1rem;
} }
.text-center {
text-align: center;
}
.text-4xl { .text-4xl {
font-size: 2.25rem; font-size: 2.25rem;
line-height: 2.5rem; line-height: 2.5rem;
@ -840,6 +868,16 @@ select {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
@ -849,12 +887,63 @@ 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 {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity));
}
.shadow { .shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.dark form label {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
.dark form input,.dark select,.dark textarea {
border-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(15 23 42 / var(--tw-border-opacity));
--tw-bg-opacity: 1;
background-color: rgb(100 116 139 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(241 245 249 / var(--tw-text-opacity));
}
form input[type=submit] {
--tw-bg-opacity: 1;
background-color: rgb(88 28 135 / var(--tw-bg-opacity));
padding: 0.5rem;
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@ -864,6 +953,11 @@ select {
border-color: rgb(255 255 255 / var(--tw-border-opacity)); border-color: rgb(255 255 255 / var(--tw-border-opacity));
} }
.dark .dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark .dark\:bg-gray-900 { .dark .dark\:bg-gray-900 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity)); background-color: rgb(17 24 39 / var(--tw-bg-opacity));
@ -874,9 +968,14 @@ select {
background-color: rgb(51 65 85 / var(--tw-bg-opacity)); background-color: rgb(51 65 85 / var(--tw-bg-opacity));
} }
.dark .dark\:text-slate-300 { .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; --tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.dark .dark\:text-slate-400 { .dark .dark\:text-slate-400 {
@ -884,6 +983,16 @@ select {
color: rgb(148 163 184 / var(--tw-text-opacity)); color: rgb(148 163 184 / var(--tw-text-opacity));
} }
.dark .dark\:text-slate-300 {
--tw-text-opacity: 1;
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;

View File

@ -6,12 +6,13 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}No Title{% endblock title %}</title> <title>Timetracker - {% block title %}Untitled{% endblock title %}</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"> <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="{% static 'base.css' %}" /> <link rel="stylesheet" href="{% static 'base.css' %}" />
</head> </head>
<body class="dark"> <body class="dark">
<div class="dark:bg-gray-800 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="#" class="flex items-center">
@ -20,7 +21,7 @@
</a> </a>
<div class="w-full md:block md:w-auto"> <div class="w-full md:block md:w-auto">
<ul <ul
class="flex flex-col md:flex-row p-4 mt-4"> 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_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>
<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>
@ -30,6 +31,9 @@
</div> </div>
</nav> </nav>
{% block content %}No content here.{% endblock %} {% block content %}No content here.{% endblock %}
</div>
{% 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>
</body> </body>
</html> </html>

View File

@ -1,17 +1,23 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Tracker Entry List{% endblock title %} {% block title %}Sessions{% endblock title %}
{% block content %} {% block content %}
{% if purchase %}
<div class="text-center text-xl mb-4 dark:text-slate-400">
<h1>Listing sessions only for purchase "{{ purchase }}"</h1>
<a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a>
</div>
{% 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-4 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>
{% for data in dataset %} {% for data in dataset %}
<div class="dark:text-slate-400">{{ data.purchase }}</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 }}</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 }}</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> <div class="dark:text-slate-400">{{ data.time_delta }}</div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -0,0 +1,18 @@
from django import template
import time
import os
register = template.Library()
@register.simple_tag
def version_date():
return time.strftime(
"%d-%b-%Y %H:%m",
time.gmtime(os.path.getmtime(os.path.abspath(os.path.join(".git")))),
)
@register.simple_tag
def version():
return os.environ.get("VERSION_NUMBER", "UNKNOWN VERSION")

View File

@ -7,4 +7,9 @@ urlpatterns = [
path("add-session/", views.add_session, name="add_session"), path("add-session/", views.add_session, name="add_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(
"list-sessions/by-purchase/<int:purchase_id>",
views.list_sessions,
name="list_sessions",
),
] ]

View File

@ -4,6 +4,7 @@ from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm from .forms import SessionForm, PurchaseForm, GameForm
from datetime import datetime from datetime import datetime
from django.db.models import ExpressionWrapper, F, DurationField from django.db.models import ExpressionWrapper, F, DurationField
import logging
def add_session(request): def add_session(request):
@ -18,9 +19,16 @@ def add_session(request):
return render(request, "add_session.html", context) return render(request, "add_session.html", context)
def list_sessions(request): def list_sessions(request, purchase_id=None):
context = {} context = {}
dataset = Session.objects.annotate(
if purchase_id != None:
dataset = Session.objects.filter(purchase=purchase_id)
context["purchase"] = Purchase.objects.get(id=purchase_id)
else:
dataset = Session.objects.all()
dataset = dataset.annotate(
time_delta=ExpressionWrapper( time_delta=ExpressionWrapper(
F("timestamp_end") - F("timestamp_start"), output_field=DurationField() F("timestamp_end") - F("timestamp_start"), output_field=DurationField()
) )

View File

@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
""" """
from pathlib import Path from pathlib import Path
import logging
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -106,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC" TIME_ZONE = os.environ.get("TZ", "UTC")
USE_I18N = True USE_I18N = True
@ -122,3 +124,13 @@ STATIC_URL = "static/"
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# https://docs.djangoproject.com/en/4.1/topics/logging/
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "WARNING"},
}
CSRF_TRUSTED_ORIGINS = ["https://tracker.kucharczyk.xyz"]