4 Commits

Author SHA1 Message Date
d9d4a3eaa1 Add more sample data
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-19 20:27:45 +01:00
8a1f66bfaf Fix entrypoint.sh 2023-01-19 20:20:50 +01:00
0224afcad9 Don't freak out if there are no sessions 2023-01-19 19:51:24 +01:00
a694406e99 Clean up naming 2023-01-19 19:46:43 +01:00
48 changed files with 67 additions and 99 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
static src/timetracker/static

View File

@ -1,11 +1,3 @@
## 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 * /usr/share/caddy root * src/timetracker/static/
file_server file_server
} }
handle { handle {
reverse_proxy backend:8001 reverse_proxy :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 ./common/input.css -o ./static/base.css --minify npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css --minify
FROM python:3.10.9-slim-bullseye FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 1.0.0 ENV VERSION_NUMBER 0.2.5
ENV PROD 1 ENV PROD 1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
@ -15,13 +15,18 @@ 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/static/base.css /home/timetracker/app/static/base.css COPY --from=css /app/src/timetracker/games/static/base.css /home/timetracker/app/src/timetracker/games/static/base.css
COPY entrypoint.sh / COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
@ -31,4 +36,4 @@ RUN pip install --no-cache-dir poetry
RUN poetry install --without dev RUN poetry install --without dev
EXPOSE 8000 EXPOSE 8000
CMD [ "/entrypoint.sh" ] ENTRYPOINT [ "/entrypoint.sh" ]

View File

@ -2,52 +2,49 @@ all: css migrate
initialize: npm css migrate sethookdir loadplatforms initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find games/templates -type f) HTMLFILES := $(shell find src/timetracker/games/templates -type f)
npm: npm:
npm install npm install
css: input.css css: src/input.css
npx tailwindcss -i ./input.css -o ./games/static/base.css npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css
css-dev: css css-dev: css
npx tailwindcss -i ./input.css -o ./games/static/base.css --watch npx tailwindcss -i ./src/input.css -o ./src/timetracker/games/static/base.css --watch
makemigrations: makemigrations:
poetry run python manage.py makemigrations poetry run python src/timetracker/manage.py makemigrations
migrate: makemigrations migrate: makemigrations
poetry run python manage.py migrate poetry run python src/timetracker/manage.py migrate
dev: migrate dev: migrate
poetry run python manage.py runserver poetry run python src/timetracker/manage.py runserver
caddy: caddy:
caddy run --watch caddy run --watch
dev-prod: migrate collectstatic dev-prod: migrate collectstatic
PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker cd src/timetracker/; 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 manage.py dumpdata --format yaml games --output tracker_fixture.yaml poetry run python src/timetracker/manage.py dumpdata --format yaml games --output tracker_fixture.yaml
loadplatforms: loadplatforms:
poetry run python manage.py loaddata platforms.yaml poetry run python src/timetracker/manage.py loaddata platforms.yaml
loadall:
poetry run python manage.py loaddata data.yaml
loadsample: loadsample:
poetry run python manage.py loaddata sample.yaml poetry run python src/timetracker/manage.py loaddata sample.yaml
createsuperuser: createsuperuser:
poetry run python manage.py createsuperuser poetry run python src/timetracker/manage.py createsuperuser
shell: shell:
poetry run python manage.py shell poetry run python src/timetracker/manage.py shell
collectstatic: collectstatic:
poetry run python manage.py collectstatic --clear --no-input poetry run python src/timetracker/manage.py collectstatic --clear --no-input
poetry.lock: pyproject.toml poetry.lock: pyproject.toml
poetry install poetry install
@ -59,6 +56,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 static/* rm -r src/timetracker/static/*
clean: cleanstatic clean: cleanstatic

View File

@ -1,17 +0,0 @@
---
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,28 +1,17 @@
--- ---
services: services:
backend: timetracker:
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:
- "static-files:/home/timetracker/app/static" # - "db:/home/timetracker/app/src/timetracker/db.sqlite3"
restart: unless-stopped
frontend:
image: caddy
volumes:
- "static-files:/usr/share/caddy"
- "$PWD/Caddyfile:/etc/caddy/Caddyfile"
ports: ports:
- "8000:8000" - "8000:8000"
depends_on: restart: unless-stopped
- backend
volumes:
static-files:

View File

@ -2,10 +2,12 @@
# Apply database migrations # Apply database migrations
set -euo pipefail set -euo pipefail
echo "Apply database migrations" echo "Apply database migrations"
poetry run python manage.py migrate poetry run python src/timetracker/manage.py migrate
echo "Collect static files" echo "Collect static files"
poetry run python manage.py collectstatic --clear --no-input poetry run python src/timetracker/manage.py collectstatic --clear --no-input
echo "Starting app" echo "Starting server"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - caddy start
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,11 +1,11 @@
[tool.poetry] [tool.poetry]
name = "timetracker" name = "timetracker"
version = "1.0.0" version = "0.2.5"
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"}] packages = [{include = "timetracker", from = "src"}]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"
@ -30,5 +30,10 @@ 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 = "common.import_data:import_from_file" timetracker-import = "timetracker.common.util.data_import:import_from_file"

View File

@ -1,8 +1,7 @@
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

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

View File

@ -1,11 +1,9 @@
from django import forms from django import forms
from games.models import Game, Platform, Purchase, Session from .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 = [
@ -18,9 +16,6 @@ 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 common.time import format_duration from timetracker.common.util.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' last.purchase.id %}"> <a class="clear-both" href="{% url 'start_session' dataset.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,7 +13,9 @@ 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.join(settings.BASE_DIR, "pyproject.toml")) os.path.abspath(
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 games import views from . 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.plots import playtime_over_time_chart from common.util.plots import playtime_over_time_chart
from common.time import now as now_with_tz from common.util.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,6 @@ 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
if Session.objects.count() >= 2:
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start")) 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
manage.py → src/timetracker/manage.py Executable file → Normal 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", "timetracker.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.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,5 +1,5 @@
""" """
ASGI config for timetracker project. ASGI config for root 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", "timetracker.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.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 = "timetracker.urls" ROOT_URLCONF = "root.urls"
TEMPLATES = [ TEMPLATES = [
{ {
@ -73,7 +73,7 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = "timetracker.wsgi.application" WSGI_APPLICATION = "root.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", "timetracker.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "root.settings")
application = get_wsgi_application() application = get_wsgi_application()

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
darkMode: 'class', darkMode: 'class',
content: ["./**/*.{html,js}"], content: ["./src/**/*.{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 common.time import format_duration from timetracker.common.util.time import format_duration
class FormatDurationTest(unittest.TestCase): class FormatDurationTest(unittest.TestCase):