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
settings:
repo: registry.kucharczyk.xyz/timetracker
tags: latest
environment:
VERSION_NUMBER: $(git describe --tags --abbrev=0)
tags:
- latest
- $VERSION_NUMBER
trigger:
event:
- push

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ __pycache__
.venv
node_modules
package-lock.json
db.sqlite3

View File

@ -1,17 +1,15 @@
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
RUN python3 -m venv pip $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip install --no-cache-dir poetry
RUN useradd --create-home --uid 1000 timetracker
RUN git clone https://git.kucharczyk.xyz/lukas/timetracker.git /home/timetracker/app
WORKDIR /home/timetracker/app
RUN chown -R timetracker /home/timetracker/app
RUN poetry install
EXPOSE 8000
COPY . /home/timetracker/app/
RUN chown -R timetracker:timetracker /home/timetracker/app
RUN poetry install --without dev
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
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]
name = "timelogger"
version = "0.1.0"
name = "timetracker"
version = "0.0.0"
description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
license = "GPL"

View File

@ -1,3 +1,15 @@
@tailwind base;
@tailwind components;
@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):
purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE)
timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField()
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
timestamp_end = models.DateTimeField(blank=True, null=True)
duration_manual = models.DurationField(blank=True, null=True)
duration_calculated = models.DurationField(blank=True, null=True)
note = models.TextField(blank=True, null=True)
@ -41,6 +41,9 @@ class Session(models.Model):
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.total_duration()}{mark})"
def calculated_duration(self):
if self.timestamp_end == None or self.timestamp_start == None:
return 0
else:
return self.timestamp_end - self.timestamp_start
def total_duration(self):

View File

@ -717,6 +717,26 @@ select {
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 {
margin-left: auto;
margin-right: auto;
@ -742,6 +762,10 @@ select {
display: grid;
}
.h-screen {
height: 100vh;
}
.w-full {
width: 100%;
}
@ -825,6 +849,10 @@ select {
padding-right: 1rem;
}
.text-center {
text-align: center;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
@ -840,6 +868,16 @@ select {
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-weight: 600;
}
@ -849,12 +887,63 @@ 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));
}
.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-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);
}
.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 {
text-decoration-line: underline;
}
@ -864,6 +953,11 @@ select {
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 {
--tw-bg-opacity: 1;
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));
}
.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;
color: rgb(203 213 225 / var(--tw-text-opacity));
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark .dark\:text-slate-400 {
@ -884,6 +983,16 @@ select {
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) {
.md\:block {
display: block;

View File

@ -6,12 +6,13 @@
<head>
<meta charset="utf-8">
<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="{% static 'base.css' %}" />
</head>
<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">
<div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="#" class="flex items-center">
@ -20,7 +21,7 @@
</a>
<div class="w-full md:block md:w-auto">
<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_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>
@ -30,6 +31,9 @@
</div>
</nav>
{% 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>
</html>

View File

@ -1,17 +1,23 @@
{% extends 'base.html' %}
{% block title %}Tracker Entry List{% endblock title %}
{% block title %}Sessions{% endblock title %}
{% 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="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">End</div>
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
{% for data in dataset %}
<div class="dark:text-slate-400">{{ data.purchase }}</div>
<div class="dark:text-slate-400">{{ data.timestamp_start }}</div>
<div class="dark:text-slate-400">{{ data.timestamp_end }}</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_end | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400">{{ data.time_delta }}</div>
{% endfor %}
</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-purchase/", views.add_purchase, name="add_purchase"),
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 datetime import datetime
from django.db.models import ExpressionWrapper, F, DurationField
import logging
def add_session(request):
@ -18,9 +19,16 @@ def add_session(request):
return render(request, "add_session.html", context)
def list_sessions(request):
def list_sessions(request, purchase_id=None):
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(
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
import logging
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -106,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
TIME_ZONE = os.environ.get("TZ", "UTC")
USE_I18N = True
@ -122,3 +124,13 @@ STATIC_URL = "static/"
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
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"]