23 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
eec8f1b9f5 Properly fix Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 14:29:39 +01:00
b695a35fb1 Rename project
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 13:26:30 +01:00
76bd923a00 Actually add entrypoint.sh
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 13:08:18 +01:00
811e7771cc Run Dockerfile via entrypoint.sh
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 13:04:05 +01:00
7d31f6f416 Remove volume before better solution is made
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-03 12:38:51 +01:00
c03b9fe8e1 make migrate in container
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 12:36:44 +01:00
ec4a095425 Don't initialize in container
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 12:34:50 +01:00
f2076a6cd0 Replace backslashes with forward slashes
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 12:14:27 +01:00
4d52fd21f8 Fix Makefile
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 00:17:23 +01:00
ab9d8d1ae3 Add make to Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 00:15:17 +01:00
9d142126b1 Do not require timestamp_end, initialize db
Some checks failed
continuous-integration/drone/push Build is failing
2023-01-03 00:13:26 +01:00
0f2f0d281e Add Makefile 2023-01-02 20:03:31 +01:00
a115adae28 Ignore db.sqlite3 2023-01-02 20:03:26 +01:00
17 changed files with 313 additions and 44 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

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
__pycache__ __pycache__
.venv .venv
node_modules node_modules
package-lock.json package-lock.json
db.sqlite3

View File

@ -1,17 +1,15 @@
FROM python:3.10-slim-bullseye FROM python:3.10-slim-bullseye
RUN apt update \
&& apt install --no-install-recommends --yes \
git \
&& rm -rf /var/lib/apt/lists/*
ENV VIRTUAL_ENV=/opt/venv ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv pip $VIRTUAL_ENV RUN python3 -m venv pip $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH" ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip install --no-cache-dir poetry RUN pip install --no-cache-dir poetry
RUN useradd --create-home --uid 1000 timetracker RUN useradd --create-home --uid 1000 timetracker
RUN git clone https://git.kucharczyk.xyz/lukas/timetracker.git /home/timetracker/app
WORKDIR /home/timetracker/app WORKDIR /home/timetracker/app
RUN chown -R timetracker /home/timetracker/app COPY . /home/timetracker/app/
RUN poetry install RUN chown -R timetracker:timetracker /home/timetracker/app
EXPOSE 8000 RUN poetry install --without dev
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
USER timetracker USER timetracker
CMD [ "python3", "src/web/manage.py", "runserver", "0.0.0.0:8000" ] EXPOSE 8000
ENTRYPOINT [ "/entrypoint.sh" ]

37
Makefile Normal file
View File

@ -0,0 +1,37 @@
.PHONY: createsuperuser
all: css migrate
initialize: npm css migrate loadplatforms
HTMLFILES := $(shell find src/web/tracker/templates -type f)
npm:
npm install
css: src/input.css
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css
css-dev: css
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --watch
makemigrations:
python src/web/manage.py makemigrations
migrate: makemigrations
python src/web/manage.py migrate
dev: migrate
python src/web/manage.py runserver
dumptracker:
python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml
loadplatforms:
python src/web/manage.py loaddata platforms.yaml
loadsample:
python src/web/manage.py loaddata sample.yaml
createsuperuser:
python src/web/manage.py createsuperuser

8
entrypoint.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
# Apply database migrations
echo "Apply database migrations"
python src/web/manage.py migrate
# Start server
echo "Starting server"
python src/web/manage.py runserver 0.0.0.0:8000

View File

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

@ -0,0 +1,21 @@
# Generated by Django 4.1.4 on 2023-01-02 18:55
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="session",
name="duration_manual",
field=models.DurationField(
blank=True, default=datetime.timedelta(0), null=True
),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.4 on 2023-01-02 23:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0002_alter_session_duration_manual"),
]
operations = [
migrations.AlterField(
model_name="session",
name="duration_manual",
field=models.DurationField(blank=True, null=True),
),
migrations.AlterField(
model_name="session",
name="timestamp_end",
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -31,8 +31,8 @@ class Platform(models.Model):
class Session(models.Model): class Session(models.Model):
purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE) purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE)
timestamp_start = models.DateTimeField() timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField() timestamp_end = models.DateTimeField(blank=True, null=True)
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) duration_manual = models.DurationField(blank=True, null=True)
duration_calculated = models.DurationField(blank=True, null=True) duration_calculated = models.DurationField(blank=True, null=True)
note = models.TextField(blank=True, null=True) note = models.TextField(blank=True, null=True)
@ -41,7 +41,10 @@ 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):
return self.timestamp_end - self.timestamp_start if self.timestamp_end == None or self.timestamp_start == None:
return 0
else:
return self.timestamp_end - self.timestamp_start
def total_duration(self): def total_duration(self):
return ( return (

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,30 +6,34 @@
<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">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded"> <div class="dark:bg-gray-800 h-screen">
<div class="container flex flex-wrap items-center justify-between mx-auto"> <nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
<a href="#" class="flex items-center"> <div class="container flex flex-wrap items-center justify-between mx-auto">
<span class="text-4xl"></span> <a href="#" class="flex items-center">
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span> <span class="text-4xl"></span>
</a> <span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
<div class="w-full md:block md:w-auto"> </a>
<ul <div class="w-full md:block md:w-auto">
class="flex flex-col md:flex-row p-4 mt-4"> <ul
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li> 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_purchase' %}">New Purchase</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_session' %}">New Session</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 'list_sessions' %}">All Sessions</a></li> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
</ul> <li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
</ul>
</div>
</div> </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"]