Compare commits
2 Commits
d211326c3f
...
1.4.0
Author | SHA1 | Date | |
---|---|---|---|
394dd4f9f8 | |||
c358b1aaa0 |
@ -1,8 +1,3 @@
|
|||||||
### Unreleased
|
|
||||||
|
|
||||||
## New
|
|
||||||
* Add stat for finished this year's games
|
|
||||||
|
|
||||||
## 1.4.0 / 2023-11-09 21:01+01:00
|
## 1.4.0 / 2023-11-09 21:01+01:00
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
@ -44,7 +44,7 @@ def format_duration(
|
|||||||
# timestamps where end is before start
|
# timestamps where end is before start
|
||||||
if seconds_total < 0:
|
if seconds_total < 0:
|
||||||
seconds_total = 0
|
seconds_total = 0
|
||||||
days = hours = hours_float = minutes = seconds = 0
|
days = hours = minutes = seconds = 0
|
||||||
remainder = seconds = seconds_total
|
remainder = seconds = seconds_total
|
||||||
if "%d" in format_string:
|
if "%d" in format_string:
|
||||||
days, remainder = divmod(seconds_total, day_seconds)
|
days, remainder = divmod(seconds_total, day_seconds)
|
||||||
@ -55,7 +55,7 @@ def format_duration(
|
|||||||
minutes, seconds = divmod(remainder, minute_seconds)
|
minutes, seconds = divmod(remainder, minute_seconds)
|
||||||
literals = {
|
literals = {
|
||||||
"d": str(days),
|
"d": str(days),
|
||||||
"H": str(hours) if "m" not in format_string else str(hours_float),
|
"H": str(hours),
|
||||||
"m": str(minutes),
|
"m": str(minutes),
|
||||||
"s": str(seconds),
|
"s": str(seconds),
|
||||||
"r": str(seconds_total),
|
"r": str(seconds_total),
|
||||||
|
@ -151,7 +151,7 @@ class Session(models.Model):
|
|||||||
objects = SessionQuerySet.as_manager()
|
objects = SessionQuerySet.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
mark = ", manual" if self.is_manual() else ""
|
mark = ", manual" if self.duration_manual != None else ""
|
||||||
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
return f"{str(self.purchase)} {str(self.timestamp_start.date())} ({self.duration_formatted()}{mark})"
|
||||||
|
|
||||||
def finish_now(self):
|
def finish_now(self):
|
||||||
@ -163,7 +163,7 @@ class Session(models.Model):
|
|||||||
def duration_seconds(self) -> timedelta:
|
def duration_seconds(self) -> timedelta:
|
||||||
manual = timedelta(0)
|
manual = timedelta(0)
|
||||||
calculated = timedelta(0)
|
calculated = timedelta(0)
|
||||||
if self.is_manual():
|
if not self.duration_manual in (None, 0, timedelta(0)):
|
||||||
manual = self.duration_manual
|
manual = self.duration_manual
|
||||||
if self.timestamp_end != None and self.timestamp_start != None:
|
if self.timestamp_end != None and self.timestamp_start != None:
|
||||||
calculated = self.timestamp_end - self.timestamp_start
|
calculated = self.timestamp_end - self.timestamp_start
|
||||||
@ -173,9 +173,6 @@ class Session(models.Model):
|
|||||||
result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
|
result = format_duration(self.duration_seconds(), "%02.0H:%02.0m")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def is_manual(self) -> bool:
|
|
||||||
return not self.duration_manual == timedelta(0)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_sum(self) -> str:
|
def duration_sum(self) -> str:
|
||||||
return Session.objects.all().total_duration_formatted()
|
return Session.objects.all().total_duration_formatted()
|
||||||
|
@ -45,10 +45,6 @@
|
|||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ all_finished_this_year.count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Finished ({{ year }})</td>
|
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ this_year_finished_this_year.count }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ urlpatterns = [
|
|||||||
name="list_sessions_recent",
|
name="list_sessions_recent",
|
||||||
),
|
),
|
||||||
path("add-game/", views.add_game, name="add_game"),
|
path("add-game/", views.add_game, name="add_game"),
|
||||||
|
path("add-game-unified/", views.add_game_unified, name="add_game_unified"),
|
||||||
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"),
|
||||||
path(
|
path(
|
||||||
|
@ -32,12 +32,7 @@ def model_counts(request):
|
|||||||
|
|
||||||
|
|
||||||
def stats_dropdown_year_range(request):
|
def stats_dropdown_year_range(request):
|
||||||
result = {
|
return {"stats_dropdown_year_range": range(2018, 2024)}
|
||||||
"stats_dropdown_year_range": range(
|
|
||||||
datetime.now(ZoneInfo(settings.TIME_ZONE)).year, 1999, -1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def add_session(request, purchase_id=None):
|
def add_session(request, purchase_id=None):
|
||||||
@ -336,7 +331,7 @@ def stats(request, year: int = 0):
|
|||||||
this_year_spendings = this_year_purchases_without_refunded.aggregate(
|
this_year_spendings = this_year_purchases_without_refunded.aggregate(
|
||||||
total_spent=Sum(F("price"))
|
total_spent=Sum(F("price"))
|
||||||
)
|
)
|
||||||
total_spent = this_year_spendings["total_spent"] or 0
|
total_spent = this_year_spendings["total_spent"]
|
||||||
|
|
||||||
games_with_playtime = (
|
games_with_playtime = (
|
||||||
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
Game.objects.filter(edition__purchase__session__in=this_year_sessions)
|
||||||
@ -385,15 +380,9 @@ def stats(request, year: int = 0):
|
|||||||
"spent_per_game": int(
|
"spent_per_game": int(
|
||||||
safe_division(total_spent, this_year_purchases_without_refunded.count())
|
safe_division(total_spent, this_year_purchases_without_refunded.count())
|
||||||
),
|
),
|
||||||
"all_finished_this_year": purchases_finished_this_year.order_by(
|
"all_finished_this_year": purchases_finished_this_year,
|
||||||
"date_finished"
|
"this_year_finished_this_year": purchases_finished_this_year_released_this_year,
|
||||||
),
|
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year,
|
||||||
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.order_by(
|
|
||||||
"date_finished"
|
|
||||||
),
|
|
||||||
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.order_by(
|
|
||||||
"date_finished"
|
|
||||||
),
|
|
||||||
"total_sessions": this_year_sessions.count(),
|
"total_sessions": this_year_sessions.count(),
|
||||||
"unique_days": unique_days["dates"],
|
"unique_days": unique_days["dates"],
|
||||||
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
|
"unique_days_percent": int(unique_days["dates"] / 365 * 100),
|
||||||
@ -488,12 +477,7 @@ def add_edition(request, game_id=None):
|
|||||||
if game_id:
|
if game_id:
|
||||||
game = Game.objects.get(id=game_id)
|
game = Game.objects.get(id=game_id)
|
||||||
form = EditionForm(
|
form = EditionForm(
|
||||||
initial={
|
initial={"game": game, "name": game.name, "sort_name": game.sort_name}
|
||||||
"game": game,
|
|
||||||
"name": game.name,
|
|
||||||
"sort_name": game.sort_name,
|
|
||||||
"year_released": game.year_released,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
form = EditionForm()
|
form = EditionForm()
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
import django
|
|
||||||
import os
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.urls import reverse
|
|
||||||
from datetime import datetime
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
|
||||||
django.setup()
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from games.models import Game, Edition, Purchase, Session, Platform
|
|
||||||
|
|
||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
class PathWorksTest(TestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
pl = Platform(name="Test Platform")
|
|
||||||
pl.save()
|
|
||||||
g = Game(name="The Test Game")
|
|
||||||
g.save()
|
|
||||||
e = Edition(game=g, name="The Test Game Edition", platform=pl)
|
|
||||||
e.save()
|
|
||||||
p = Purchase(
|
|
||||||
edition=e,
|
|
||||||
platform=pl,
|
|
||||||
date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
|
||||||
)
|
|
||||||
p.save()
|
|
||||||
s = Session(
|
|
||||||
purchase=p,
|
|
||||||
timestamp_start=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
|
||||||
timestamp_end=datetime(2022, 9, 26, 17, 38, tzinfo=ZONEINFO),
|
|
||||||
)
|
|
||||||
s.save()
|
|
||||||
self.testSession = s
|
|
||||||
return super().setUp()
|
|
||||||
|
|
||||||
def test_add_device_returns_200(self):
|
|
||||||
url = reverse("add_device")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_add_platform_returns_200(self):
|
|
||||||
url = reverse("add_platform")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_add_game_returns_200(self):
|
|
||||||
url = reverse("add_game")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_add_edition_returns_200(self):
|
|
||||||
url = reverse("add_edition")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_add_purchase_returns_200(self):
|
|
||||||
url = reverse("add_purchase")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_add_session_returns_200(self):
|
|
||||||
url = reverse("add_session")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_edit_session_returns_200(self):
|
|
||||||
id = self.testSession.id
|
|
||||||
url = reverse("edit_session", args=[id])
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_view_game_returns_200(self):
|
|
||||||
url = reverse("view_game", args=[1])
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_edit_game_returns_200(self):
|
|
||||||
url = reverse("edit_game", args=[1])
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_list_sessions_returns_200(self):
|
|
||||||
url = reverse("list_sessions")
|
|
||||||
response = self.client.get(url)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,38 +0,0 @@
|
|||||||
import django
|
|
||||||
import os
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.db import models
|
|
||||||
from datetime import datetime
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
|
|
||||||
django.setup()
|
|
||||||
from django.conf import settings
|
|
||||||
from games.models import Game, Edition, Purchase, Session
|
|
||||||
|
|
||||||
ZONEINFO = ZoneInfo(settings.TIME_ZONE)
|
|
||||||
|
|
||||||
|
|
||||||
class FormatDurationTest(TestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
return super().setUp()
|
|
||||||
|
|
||||||
def test_duration_format(self):
|
|
||||||
g = Game(name="The Test Game")
|
|
||||||
g.save()
|
|
||||||
e = Edition(game=g, name="The Test Game Edition")
|
|
||||||
e.save()
|
|
||||||
p = Purchase(
|
|
||||||
edition=e, date_purchased=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO)
|
|
||||||
)
|
|
||||||
p.save()
|
|
||||||
s = Session(
|
|
||||||
purchase=p,
|
|
||||||
timestamp_start=datetime(2022, 9, 26, 14, 58, tzinfo=ZONEINFO),
|
|
||||||
timestamp_end=datetime(2022, 9, 26, 17, 38, tzinfo=ZONEINFO),
|
|
||||||
)
|
|
||||||
s.save()
|
|
||||||
self.assertEqual(
|
|
||||||
s.duration_formatted(),
|
|
||||||
"02:40",
|
|
||||||
)
|
|
@ -83,16 +83,6 @@ class FormatDurationTest(unittest.TestCase):
|
|||||||
result = format_duration(delta, "%r seconds")
|
result = format_duration(delta, "%r seconds")
|
||||||
self.assertEqual(result, "0 seconds")
|
self.assertEqual(result, "0 seconds")
|
||||||
|
|
||||||
def test_specific(self):
|
|
||||||
delta = timedelta(hours=2, minutes=40)
|
|
||||||
result = format_duration(delta, "%H:%m")
|
|
||||||
self.assertEqual(result, "2:40")
|
|
||||||
|
|
||||||
def test_specific_precise_if_unncessary(self):
|
|
||||||
delta = timedelta(hours=2, minutes=40)
|
|
||||||
result = format_duration(delta, "%02.0H:%02.0m")
|
|
||||||
self.assertEqual(result, "02:40")
|
|
||||||
|
|
||||||
def test_all_at_once(self):
|
def test_all_at_once(self):
|
||||||
delta = timedelta(days=50, hours=10, minutes=34, seconds=24)
|
delta = timedelta(days=50, hours=10, minutes=34, seconds=24)
|
||||||
result = format_duration(
|
result = format_duration(
|
||||||
|
@ -150,3 +150,5 @@ if _csrf_trusted_origins:
|
|||||||
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
||||||
else:
|
else:
|
||||||
CSRF_TRUSTED_ORIGINS = []
|
CSRF_TRUSTED_ORIGINS = []
|
||||||
|
|
||||||
|
USE_L10N = False
|
||||||
|
Reference in New Issue
Block a user