Compare commits

..

No commits in common. "2553d6f9e606a1b292a20937e9205800dbe4725e" and "6b7ed0dbb5876e205b603905d54e761f0389cab4" have entirely different histories.

22 changed files with 61 additions and 119 deletions

0
.githooks/pre-commit.disabled → .githooks/pre-commit Normal file → Executable file
View File

View File

@ -1,11 +1,3 @@
## 0.2.4 / 2023-01-16 19:39+01:00
* Fixed
* When filtering by game, the "Filtering by (...)" text would erroneously list an unrelated platform
* Playtime graph would display timeline backwards
* Playtime graph with many dates would overlap (https://git.kucharczyk.xyz/lukas/timetracker/issues/34)
* Manually added times (= without end timestamp) would make graphs look ugly and noisy (https://git.kucharczyk.xyz/lukas/timetracker/issues/35)
## 0.2.3 / 2023-01-15 23:13+01:00
* Allow filtering by platform and game on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/32)

View File

@ -6,7 +6,7 @@ RUN npm install && \
FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 0.2.4
ENV VERSION_NUMBER 0.2.3
ENV PROD 1
ENV PYTHONUNBUFFERED=1

View File

@ -19,13 +19,13 @@ makemigrations:
migrate: makemigrations
poetry run python src/web/manage.py migrate
dev: migrate
dev: migrate sethookdir
poetry run python src/web/manage.py runserver
caddy:
caddy run --watch
dev-prod: migrate collectstatic
dev-prod: migrate collectstatic sethookdir
cd src/web/; PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 web.asgi:application -k uvicorn.workers.UvicornWorker
dumptracker:
@ -52,6 +52,9 @@ poetry.lock: pyproject.toml
test: poetry.lock
poetry run pytest
sethookdir:
git config core.hooksPath .githooks
date:
poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'

20
poetry.lock generated
View File

@ -417,24 +417,6 @@ files = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
[[package]]
name = "isort"
version = "5.11.4"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jsbeautifier"
version = "1.14.7"
@ -1297,4 +1279,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "bf02e951b8c14fbe6f2709b6df7f9664f99ad5fbf7195d8e7b18c8574d00e683"
content-hash = "01d5c9b89b638c993f8540298dedfa79321b3aac1b2af70da58ef77706d0a113"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "timetracker"
version = "0.2.4"
version = "0.2.3"
description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
license = "GPL"
@ -23,7 +23,6 @@ django-extensions = "^3.2.1"
werkzeug = "^2.2.2"
djhtml = "^1.5.2"
djlint = "^1.19.11"
isort = "^5.11.4"
[build-system]
requires = ["poetry-core"]

View File

@ -1,13 +1,14 @@
import base64
from datetime import datetime
from io import BytesIO
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
from django.db.models import F, IntegerField, QuerySet, Sum
from django.db.models.functions import TruncDay
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import base64
from io import BytesIO
from tracker.models import Session
from django.db.models import Sum, IntegerField, F
from django.db.models.functions import TruncDay
import logging
from datetime import datetime
from django.db.models import QuerySet
def key_value_to_value_value(data):
@ -17,12 +18,11 @@ def key_value_to_value_value(data):
def playtime_over_time_chart(queryset: QuerySet = Session.objects):
microsecond_in_second = 1000000
result = (
queryset.exclude(timestamp_end__exact=None)
.annotate(date=TruncDay("timestamp_start"))
queryset.annotate(date=TruncDay("timestamp_start"))
.values("date")
.annotate(
hours=Sum(
F("duration_calculated"),
F("duration_calculated") + F("duration_manual"),
output_field=IntegerField(),
)
)
@ -32,18 +32,12 @@ def playtime_over_time_chart(queryset: QuerySet = Session.objects):
values = []
running_total = int(0)
for item in result:
# date_value = datetime.strftime(item["date"], "%d-%m-%Y")
date_value = item["date"]
date_value = datetime.strftime(item["date"], "%d-%m-%Y")
keys.append(date_value)
running_total += int(item["hours"] / (3600 * microsecond_in_second))
values.append(running_total)
data = [keys, values]
return get_chart(
data,
title="Playtime over time (manual excluded)",
xlabel="Date",
ylabel="Hours",
)
return get_chart(data, title="Playtime over time", xlabel="Date", ylabel="Hours")
def get_graph():
@ -58,34 +52,13 @@ def get_graph():
def get_chart(data, title="", xlabel="", ylabel=""):
x = data[0]
y = data[1]
plt.style.use("dark_background")
plt.switch_backend("SVG")
fig, ax = plt.subplots()
fig.set_size_inches(10, 4)
ax.plot(x, y)
first = x[0]
last = x[-1]
difference = last - first
if difference.days <= 14:
ax.xaxis.set_major_locator(mdates.DayLocator())
elif difference.days < 60 or len(x) < 60:
ax.xaxis.set_major_locator(mdates.WeekdayLocator())
ax.xaxis.set_minor_locator(mdates.DayLocator())
elif difference.days < 720:
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
else:
ax.xaxis.set_major_locator(mdates.YearLocator())
ax.xaxis.set_minor_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
for label in ax.get_xticklabels(which="major"):
label.set(rotation=30, horizontalalignment="right")
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_title(title)
fig.tight_layout()
fig = plt.figure(figsize=(10, 4))
plt.plot(data[0], data[1])
plt.title(title)
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.tight_layout()
chart = get_graph()
return chart

View File

@ -1,8 +1,7 @@
import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
from zoneinfo import ZoneInfo
import re
def now() -> datetime:

View File

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

View File

@ -1,6 +1,5 @@
from django import forms
from .models import Game, Platform, Purchase, Session
from .models import Session, Purchase, Game, Platform
class SessionForm(forms.ModelForm):

View File

@ -1,7 +1,7 @@
# Generated by Django 4.1.4 on 2023-01-02 18:27
import django.db.models.deletion
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):

View File

@ -1,7 +1,6 @@
# Generated by Django 4.1.4 on 2023-01-02 18:55
import datetime
from django.db import migrations, models

View File

@ -1,7 +1,6 @@
# Generated by Django 4.1.5 on 2023-01-09 14:49
import datetime
from django.db import migrations, models

View File

@ -1,8 +1,7 @@
# Generated by Django 4.1.5 on 2023-01-09 17:43
from datetime import timedelta
from django.db import migrations
from datetime import timedelta
def set_duration_calculated_none_to_zero(apps, schema_editor):

View File

@ -1,8 +1,7 @@
# Generated by Django 4.1.5 on 2023-01-09 18:04
from datetime import timedelta
from django.db import migrations
from datetime import timedelta
def set_duration_manual_none_to_zero(apps, schema_editor):

View File

@ -1,11 +1,11 @@
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo
from common.util.time import format_duration
from django.conf import settings
from django.db import models
from django.db.models import F, Manager, Sum
from datetime import datetime, timedelta
from django.conf import settings
from zoneinfo import ZoneInfo
from common.util.time import format_duration
from django.db.models import Sum, F
from django.db.models import Manager
from typing import Any
class Game(models.Model):

View File

@ -10,11 +10,11 @@
{% if dataset.count >= 1 %}
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
{% endif %}
{% if purchase or platform or game %}
{% if purchase or platform %}
<a class="text-red-400 inline" href="{% url 'list_sessions' %}"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a><span>Filtering by "{% firstof purchase platform game %}"</span>
</a><span>Filtering by "{% firstof purchase platform %}"</span>
{% 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 %}
{% if dataset.count >= 1 %}

View File

@ -1,8 +1,7 @@
import os
import time
from django import template
from django.conf import settings
import time
import os
register = template.Library()

View File

@ -1,13 +1,14 @@
from datetime import datetime
from zoneinfo import ZoneInfo
from django.shortcuts import render, redirect
from common.util.plots import playtime_over_time_chart
from common.util.time import now as now_with_tz
from django.conf import settings
from django.shortcuts import redirect, render
from .forms import GameForm, PlatformForm, PurchaseForm, SessionForm
from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
from common.util.time import now as now_with_tz, format_duration
from django.db.models import Sum
import logging
from common.util.plots import playtime_over_time_chart
def model_counts(request):
@ -63,7 +64,7 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
context["platform"] = Platform.objects.get(id=platform_id)
elif filter == "game":
dataset = Session.objects.filter(purchase__game=game_id)
context["game"] = Game.objects.get(id=game_id)
context["game"] = Platform.objects.get(id=game_id)
else:
dataset = Session.objects.all().order_by("-timestamp_start")
@ -74,8 +75,7 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
context["total_duration"] = dataset.total_duration()
context["dataset"] = dataset
# charts are always oldest->newest
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
context["chart"] = playtime_over_time_chart(dataset)
return render(request, "list_sessions.html", context)

View File

@ -10,8 +10,9 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import os
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

View File

@ -13,10 +13,11 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from django.conf import settings
urlpatterns = [
path("", RedirectView.as_view(url="/tracker/list-sessions")),

View File

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