9 Commits

Author SHA1 Message Date
215374167b Version 1.0.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 19:58:40 +01:00
77268ae92f Add make loadall 2023-01-20 19:58:31 +01:00
c42687a072 Change ENTRYPOINT to CMD 2023-01-20 19:58:09 +01:00
ca16345374 Fix start session button starting different game than it says
Fixes #44
2023-01-20 19:57:45 +01:00
3a3045be91 Sort form fields alphabetically
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #39
Fixes #40
2023-01-20 18:27:30 +01:00
d40612af72 Remove Caddy
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 17:15:53 +01:00
18e8f93261 Additional fixes
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 15:06:42 +01:00
56e5dfaa03 Rename project, part 2 (#42)
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #42
2023-01-20 13:37:46 +00:00
2f00be455d Rename project (#41)
All checks were successful
continuous-integration/drone/push Build is passing
The old naming scheme was causing confusion and probably errors.

Reviewed-on: #41
2023-01-19 19:35:25 +00:00
48 changed files with 99 additions and 67 deletions

2
.gitignore vendored
View File

@ -5,4 +5,4 @@ __pycache__
node_modules node_modules
package-lock.json package-lock.json
db.sqlite3 db.sqlite3
src/timetracker/static static

View File

@ -1,3 +1,11 @@
## 1.0.0 / 2023-01-20 19:54+01:00
* Breaking
* Due to major re-arranging and re-naming of the folder structure, tables also had to be renamed.
* Fixed
* Sort form fields alphabetically (https://git.kucharczyk.xyz/lukas/timetracker/issues/39, https://git.kucharczyk.xyz/lukas/timetracker/issues/40)
* Start session button starts different game than it says (#44)
## 0.2.5 / 2023-01-18 17:01+01:00 ## 0.2.5 / 2023-01-18 17:01+01:00
* New * New

View File

@ -5,10 +5,10 @@
:8000 { :8000 {
handle_path /static/* { handle_path /static/* {
root * src/timetracker/static/ root * /usr/share/caddy
file_server file_server
} }
handle { handle {
reverse_proxy :8001 reverse_proxy backend:8001
} }
} }

View File

@ -2,11 +2,11 @@ FROM node as css
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN npm install && \ RUN npm install && \
npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css --minify npx tailwindcss -i ./common/input.css -o ./static/base.css --minify
FROM python:3.10.9-slim-bullseye FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 0.2.5 ENV VERSION_NUMBER 1.0.0
ENV PROD 1 ENV PROD 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -15,18 +15,13 @@ RUN apt update && \
bash \ bash \
vim \ vim \
curl && \ curl && \
apt install -y debian-keyring debian-archive-keyring apt-transport-https && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && \
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list && \
apt update && \
apt install caddy && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
RUN useradd -m --uid 1000 timetracker RUN useradd -m --uid 1000 timetracker
WORKDIR /home/timetracker/app WORKDIR /home/timetracker/app
COPY . /home/timetracker/app/ COPY . /home/timetracker/app/
RUN chown -R timetracker:timetracker /home/timetracker/app RUN chown -R timetracker:timetracker /home/timetracker/app
COPY --from=css /app/src/timetracker/games/static/base.css /home/timetracker/app/src/timetracker/games/static/base.css COPY --from=css ./app/static/base.css /home/timetracker/app/static/base.css
COPY entrypoint.sh / COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
@ -36,4 +31,4 @@ RUN pip install --no-cache-dir poetry
RUN poetry install --without dev RUN poetry install --without dev
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT [ "/entrypoint.sh" ] CMD [ "/entrypoint.sh" ]

View File

@ -2,49 +2,52 @@ all: css migrate
initialize: npm css migrate sethookdir loadplatforms initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find src/timetracker/games/templates -type f) HTMLFILES := $(shell find games/templates -type f)
npm: npm:
npm install npm install
css: src/input.css css: input.css
npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css npx tailwindcss -i ./input.css -o ./games/static/base.css
css-dev: css css-dev: css
npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css --watch npx tailwindcss -i ./input.css -o ./games/static/base.css --watch
makemigrations: makemigrations:
poetry run python src/timetracker/manage.py makemigrations poetry run python manage.py makemigrations
migrate: makemigrations migrate: makemigrations
poetry run python src/timetracker/manage.py migrate poetry run python manage.py migrate
dev: migrate dev: migrate
poetry run python src/timetracker/manage.py runserver poetry run python manage.py runserver
caddy: caddy:
caddy run --watch caddy run --watch
dev-prod: migrate collectstatic dev-prod: migrate collectstatic
cd src/timetracker/; PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker
dumpgames: dumpgames:
poetry run python src/timetracker/manage.py dumpdata --format yaml games --output tracker_fixture.yaml poetry run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
loadplatforms: loadplatforms:
poetry run python src/timetracker/manage.py loaddata platforms.yaml poetry run python manage.py loaddata platforms.yaml
loadall:
poetry run python manage.py loaddata data.yaml
loadsample: loadsample:
poetry run python src/timetracker/manage.py loaddata sample.yaml poetry run python manage.py loaddata sample.yaml
createsuperuser: createsuperuser:
poetry run python src/timetracker/manage.py createsuperuser poetry run python manage.py createsuperuser
shell: shell:
poetry run python src/timetracker/manage.py shell poetry run python manage.py shell
collectstatic: collectstatic:
poetry run python src/timetracker/manage.py collectstatic --clear --no-input poetry run python manage.py collectstatic --clear --no-input
poetry.lock: pyproject.toml poetry.lock: pyproject.toml
poetry install poetry install
@ -56,6 +59,6 @@ date:
poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))' poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'
cleanstatic: cleanstatic:
rm -r src/timetracker/static/* rm -r static/*
clean: cleanstatic clean: cleanstatic

View File

@ -1,7 +1,8 @@
from timetracker.games.models import Session, Game, Purchase, Platform
import csv import csv
from typing import TypeAlias from typing import TypeAlias
from games.models import Game
DataList: TypeAlias = list[dict[str, str]] | None DataList: TypeAlias = list[dict[str, str]] | None

View File

@ -0,0 +1,17 @@
---
services:
timetracker:
image: registry.kucharczyk.xyz/timetracker
build:
context: .
dockerfile: Dockerfile
container_name: timetracker
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
# volumes:
# - "db:/home/timetracker/app/src/timetracker/db.sqlite3"
ports:
- "8000:8000"
restart: unless-stopped

View File

@ -1,17 +1,28 @@
--- ---
services: services:
timetracker: backend:
image: registry.kucharczyk.xyz/timetracker image: registry.kucharczyk.xyz/timetracker
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: timetracker
environment: environment:
- TZ=Europe/Prague - TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz" - CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000" user: "1000"
# volumes: volumes:
# - "db:/home/timetracker/app/src/timetracker/db.sqlite3" - "static-files:/home/timetracker/app/static"
restart: unless-stopped
frontend:
image: caddy
volumes:
- "static-files:/usr/share/caddy"
- "$PWD/Caddyfile:/etc/caddy/Caddyfile"
ports: ports:
- "8000:8000" - "8000:8000"
restart: unless-stopped depends_on:
- backend
volumes:
static-files:

View File

@ -2,12 +2,10 @@
# Apply database migrations # Apply database migrations
set -euo pipefail set -euo pipefail
echo "Apply database migrations" echo "Apply database migrations"
poetry run python src/timetracker/manage.py migrate poetry run python manage.py migrate
echo "Collect static files" echo "Collect static files"
poetry run python src/timetracker/manage.py collectstatic --clear --no-input poetry run python manage.py collectstatic --clear --no-input
echo "Starting server" echo "Starting app"
caddy start poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -
cd src/timetracker || exit
poetry run python -m gunicorn --bind 0.0.0.0:8001 root.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -

View File

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Game, Platform, Purchase, Session from games.models import Game, Platform, Purchase, Session
# Register your models here. # Register your models here.
admin.site.register(Game) admin.site.register(Game)

View File

@ -222,4 +222,4 @@ Remnant: From the Ashes,PS4,2021-03-08 02:41,2021-03-08 05:38
Remnant: From the Ashes,PS4,2021-03-07 03:21,2021-03-07 06:49 Remnant: From the Ashes,PS4,2021-03-07 03:21,2021-03-07 06:49
13 Sentinels: Aegis Rim,PS4,2021-03-07 03:20,2021-03-07 03:21 13 Sentinels: Aegis Rim,PS4,2021-03-07 03:20,2021-03-07 03:21
DARK SOULS™ II: Scholar of the First Sin,PS4,2020-10-24 23:43,2020-10-25 01:18 DARK SOULS™ II: Scholar of the First Sin,PS4,2020-10-24 23:43,2020-10-25 01:18
Ghost of Tsushima,PS4,2020-10-24 23:14,2020-10-24 23:22 Ghost of Tsushima,PS4,2020-10-24 23:14,2020-10-24 23:22
1 name platform start end
222 Remnant: From the Ashes PS4 2021-03-07 03:21 2021-03-07 06:49
223 13 Sentinels: Aegis Rim PS4 2021-03-07 03:20 2021-03-07 03:21
224 DARK SOULS™ II: Scholar of the First Sin PS4 2020-10-24 23:43 2020-10-25 01:18
225 Ghost of Tsushima PS4 2020-10-24 23:14 2020-10-24 23:22

View File

@ -1,9 +1,11 @@
from django import forms from django import forms
from .models import Game, Platform, Purchase, Session from games.models import Game, Platform, Purchase, Session
class SessionForm(forms.ModelForm): class SessionForm(forms.ModelForm):
purchase = forms.ModelChoiceField(queryset=Purchase.objects.order_by("game__name"))
class Meta: class Meta:
model = Session model = Session
fields = [ fields = [
@ -16,6 +18,9 @@ class SessionForm(forms.ModelForm):
class PurchaseForm(forms.ModelForm): class PurchaseForm(forms.ModelForm):
game = forms.ModelChoiceField(queryset=Game.objects.order_by("name"))
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
class Meta: class Meta:
model = Purchase model = Purchase
fields = ["game", "platform", "date_purchased", "date_refunded"] fields = ["game", "platform", "date_purchased", "date_refunded"]

View File

@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from typing import Any from typing import Any
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from timetracker.common.util.time import format_duration from common.time import format_duration
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.db.models import F, Manager, Sum from django.db.models import F, Manager, Sum

View File

@ -18,7 +18,7 @@
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_game' purchase.game.id %}">See all platforms</a>{% endif %} {% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_game' purchase.game.id %}">See all platforms</a>{% endif %}
{% endif %} {% endif %}
{% if dataset.count >= 1 %} {% if dataset.count >= 1 %}
<a class="clear-both" href="{% url 'start_session' dataset.last.purchase.id %}"> <a class="clear-both" href="{% url 'start_session' last.purchase.id %}">
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg "> <button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />

View File

@ -13,9 +13,7 @@ def version_date():
"%d-%b-%Y %H:%m", "%d-%b-%Y %H:%m",
time.gmtime( time.gmtime(
os.path.getmtime( os.path.getmtime(
os.path.abspath( os.path.abspath(os.path.join(settings.BASE_DIR, "pyproject.toml"))
os.path.join(settings.BASE_DIR, "..", "..", "pyproject.toml")
)
) )
), ),
) )

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from . import views from games import views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),

View File

@ -1,8 +1,8 @@
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from common.util.plots import playtime_over_time_chart from common.plots import playtime_over_time_chart
from common.util.time import now as now_with_tz from common.time import now as now_with_tz
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
@ -85,7 +85,8 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
# cannot use dataset[0] here because that might be only partial QuerySet # cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.all().order_by("timestamp_start").last() context["last"] = Session.objects.all().order_by("timestamp_start").last()
# charts are always oldest->newest # charts are always oldest->newest
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start")) if Session.objects.count() >= 2:
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
return render(request, "list_sessions.html", context) return render(request, "list_sessions.html", context)

2
src/timetracker/manage.py → manage.py Normal file → Executable file
View File

@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@ -1,11 +1,11 @@
[tool.poetry] [tool.poetry]
name = "timetracker" name = "timetracker"
version = "0.2.5" version = "1.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"
readme = "README.md" readme = "README.md"
packages = [{include = "timetracker", from = "src"}] packages = [{include = "timetracker"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
@ -30,10 +30,5 @@ isort = "^5.11.4"
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
pythonpath = [
"src"
]
[tool.poetry.scripts] [tool.poetry.scripts]
timetracker-import = "timetracker.common.util.data_import:import_from_file" timetracker-import = "common.import_data:import_from_file"

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
darkMode: 'class', darkMode: 'class',
content: ["./src/**/*.{html,js}"], content: ["./**/*.{html,js}"],
theme: { theme: {
fontFamily: { fontFamily: {
sans: ['Inter', 'sans-serif'], sans: ['Inter', 'sans-serif'],

View File

@ -1,7 +1,7 @@
import unittest import unittest
from datetime import timedelta from datetime import timedelta
from timetracker.common.util.time import format_duration from common.time import format_duration
class FormatDurationTest(unittest.TestCase): class FormatDurationTest(unittest.TestCase):

View File

@ -1,5 +1,5 @@
""" """
ASGI config for root project. ASGI config for timetracker project.
It exposes the ASGI callable as a module-level variable named ``application``. It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
application = get_asgi_application() application = get_asgi_application()

View File

@ -54,7 +54,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = "root.urls" ROOT_URLCONF = "timetracker.urls"
TEMPLATES = [ TEMPLATES = [
{ {
@ -73,7 +73,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = "root.wsgi.application" WSGI_APPLICATION = "timetracker.wsgi.application"
# Database # Database

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
application = get_wsgi_application() application = get_wsgi_application()