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
49 changed files with 90 additions and 162 deletions

View File

@ -11,30 +11,15 @@ steps:
- poetry install - poetry install
- poetry env info - poetry env info
- poetry run pytest - poetry run pytest
- name: build container (prod) - name: build container
image: plugins/docker image: plugins/docker
settings: settings:
repo: registry.kucharczyk.xyz/timetracker repo: registry.kucharczyk.xyz/timetracker
tags: tags:
- latest - latest
when:
branch:
- main
- name: build container (non-prod)
image: plugins/docker
settings:
repo: registry.kucharczyk.xyz/timetracker
tags:
- ${DRONE_COMMIT_REF}
- ${DRONE_COMMIT_BRANCH}
when:
branch:
exclude:
- main
trigger: trigger:
event: event:
- push - push
- cron - cron
exclude:
- pull_request

3
.gitignore vendored
View File

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

View File

@ -1,17 +1,3 @@
## 1.0.1 / 2023-01-30 22:17+01:00
* Make it possible to edit sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/46)
* Show markers on smaller graphs to make it clearer which dates the session belong to
* Show only last 30 days on the homepage (https://git.kucharczyk.xyz/lukas/timetracker/issues/47)
## 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.1" 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

@ -64,7 +64,7 @@ def get_chart(data, title="", xlabel="", ylabel=""):
plt.switch_backend("SVG") plt.switch_backend("SVG")
fig, ax = plt.subplots() fig, ax = plt.subplots()
fig.set_size_inches(10, 4) fig.set_size_inches(10, 4)
lines = ax.plot(x, y, "-o") ax.plot(x, y)
first = x[0] first = x[0]
last = x[-1] last = x[-1]
difference = last - first difference = last - first
@ -76,11 +76,7 @@ def get_chart(data, title="", xlabel="", ylabel=""):
elif difference.days < 720: elif difference.days < 720:
ax.xaxis.set_major_locator(mdates.MonthLocator()) ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_minor_locator(mdates.WeekdayLocator()) ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
for line in lines:
line.set_marker("")
else: else:
for line in lines:
line.set_marker("")
ax.xaxis.set_major_locator(mdates.YearLocator()) ax.xaxis.set_major_locator(mdates.YearLocator())
ax.xaxis.set_minor_locator(mdates.MonthLocator()) ax.xaxis.set_minor_locator(mdates.MonthLocator())

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

@ -17,7 +17,7 @@
<div class="dark:bg-gray-800 min-h-screen"> <div class="dark:bg-gray-800 min-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="{% url 'list_sessions_recent' %}" class="flex items-center"> <a href="{% url 'index' %}" class="flex items-center">
<span class="text-4xl"></span> <span class="text-4xl"></span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span> <span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a> </a>

View File

@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}{{ title }}{% endblock title %} {% block title %}Sessions{% endblock title %}
{% block content %} {% block content %}
<div class="text-center text-xl mb-4 dark:text-slate-400"> <div class="text-center text-xl mb-4 dark:text-slate-400">
@ -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" />
@ -62,14 +62,12 @@
</button> </button>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'edit_session' data.id %}"> <button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-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 w-7 h-4 rounded-lg ">
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-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 w-7 h-4 rounded-lg "> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
<path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" /> <path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" /> </svg>
</svg> </button>
</button>
</a>
<a href="{% url 'delete_session' data.id %}"> <a href="{% url 'delete_session' data.id %}">
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-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 w-7 h-4 rounded-lg "> <button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-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 w-7 h-4 rounded-lg ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">

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,9 +1,9 @@
from django.urls import path from django.urls import path
from games import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.list_sessions, {"filter": "recent"}, name="list_sessions_recent"), path("", views.index, name="index"),
path("add-game/", views.add_game, name="add_game"), path("add-game/", views.add_game, name="add_game"),
path("add-platform/", views.add_platform, name="add_platform"), path("add-platform/", views.add_platform, name="add_platform"),
path("add-session/", views.add_session, name="add_session"), path("add-session/", views.add_session, name="add_session"),
@ -23,7 +23,6 @@ urlpatterns = [
name="delete_session", name="delete_session",
), ),
path("add-purchase/", views.add_purchase, name="add_purchase"), path("add-purchase/", views.add_purchase, name="add_purchase"),
path("edit-session/<int:session_id>", views.edit_session, name="edit_session"),
path("list-sessions/", views.list_sessions, name="list_sessions"), path("list-sessions/", views.list_sessions, name="list_sessions"),
path( path(
"list-sessions/by-purchase/<int:purchase_id>", "list-sessions/by-purchase/<int:purchase_id>",

View File

@ -1,8 +1,8 @@
from datetime import datetime, timedelta 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
@ -47,18 +47,6 @@ def update_session(request, session_id=None):
return redirect("list_sessions") return redirect("list_sessions")
def edit_session(request, session_id=None):
context = {}
session = Session.objects.get(id=session_id)
form = SessionForm(request.POST or None, instance=session)
if form.is_valid():
form.save()
return redirect("list_sessions")
context["title"] = "Edit Session"
context["form"] = form
return render(request, "add.html", context)
def start_session(request, purchase_id=None): def start_session(request, purchase_id=None):
session = SessionForm({"purchase": purchase_id, "timestamp_start": now_with_tz()}) session = SessionForm({"purchase": purchase_id, "timestamp_start": now_with_tz()})
session.save() session.save()
@ -73,7 +61,6 @@ def delete_session(request, session_id=None):
def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""): def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""):
context = {} context = {}
context["title"] = "Sessions"
if filter == "purchase": if filter == "purchase":
dataset = Session.objects.filter(purchase=purchase_id) dataset = Session.objects.filter(purchase=purchase_id)
@ -84,11 +71,6 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
elif filter == "game": elif filter == "game":
dataset = Session.objects.filter(purchase__game=game_id) dataset = Session.objects.filter(purchase__game=game_id)
context["game"] = Game.objects.get(id=game_id) context["game"] = Game.objects.get(id=game_id)
elif filter == "recent":
dataset = Session.objects.filter(
timestamp_start__gte=datetime.now() - timedelta(days=30)
)
context["title"] = "Last 30 days"
else: else:
# by default, sort from newest to oldest # by default, sort from newest to oldest
dataset = Session.objects.all().order_by("-timestamp_start") dataset = Session.objects.all().order_by("-timestamp_start")
@ -103,8 +85,7 @@ 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)
@ -145,3 +126,10 @@ def add_platform(request):
context["form"] = form context["form"] = form
context["title"] = "Add New Platform" context["title"] = "Add New Platform"
return render(request, "add.html", context) return render(request, "add.html", context)
def index(request):
context = {}
context["total_duration"] = Session().duration_sum
context["title"] = "Index"
return render(request, "index.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

@ -19,7 +19,7 @@ from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
urlpatterns = [ urlpatterns = [
path("", RedirectView.as_view(url="/tracker")), path("", RedirectView.as_view(url="/tracker/list-sessions")),
path("tracker/", include("games.urls")), path("tracker/", include("games.urls")),
] ]

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

@ -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,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):