Merge branch 'main' of github.com:KucharczykL/timetracker
Django CI/CD / test (push) Failing after 13m50s
Django CI/CD / build-and-push (push) Has been skipped

This commit is contained in:
2026-06-07 20:22:50 +02:00
6 changed files with 197 additions and 46 deletions
+21 -1
View File
@@ -7,11 +7,13 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import now as django_timezone_now
from ninja import Field, ModelSchema, NinjaAPI, Router, Schema, Status
from games.models import Game, PlayEvent, Session
from games.models import Device, Game, Platform, PlayEvent, Session
api = NinjaAPI()
playevent_router = Router()
game_router = Router()
device_router = Router()
platform_router = Router()
NOW_FACTORY = django_timezone_now
@@ -115,8 +117,26 @@ def delete_playevent(request, playevent_id: int):
return Status(204, None)
@device_router.get("/search", response=list[GameOption])
def search_devices(request, q: str = "", limit: int = 10):
qs = Device.objects.order_by("name")
if q:
qs = qs.filter(name__icontains=q)
return [{"value": d.id, "label": d.name, "data": {}} for d in qs[:limit]]
@platform_router.get("/search", response=list[GameOption])
def search_platforms(request, q: str = "", limit: int = 10):
qs = Platform.objects.order_by("name")
if q:
qs = qs.filter(name__icontains=q)
return [{"value": p.id, "label": p.name, "data": {}} for p in qs[:limit]]
api.add_router("/playevent", playevent_router)
api.add_router("/games", game_router)
api.add_router("/devices", device_router)
api.add_router("/platforms", platform_router)
session_router = Router()
+49 -8
View File
@@ -46,6 +46,20 @@ def _game_options(values) -> list[SearchSelectOption]:
]
def _device_options(values) -> list[SearchSelectOption]:
return [
{"value": d.id, "label": d.name, "data": {}}
for d in Device.objects.filter(pk__in=values)
]
def _platform_options(values) -> list[SearchSelectOption]:
return [
{"value": p.id, "label": p.name, "data": {}}
for p in Platform.objects.filter(pk__in=values)
]
class SearchSelectWidget(forms.Widget):
"""Thin Django adapter that renders a `SearchSelect()` component.
@@ -57,6 +71,7 @@ class SearchSelectWidget(forms.Widget):
self,
*,
search_url,
options_resolver,
multi_select=False,
items_visible=5,
items_scroll=10,
@@ -66,6 +81,7 @@ class SearchSelectWidget(forms.Widget):
):
super().__init__(attrs)
self.search_url = search_url
self.options_resolver = options_resolver
self.multi_select = multi_select
self.items_visible = items_visible
self.items_scroll = items_scroll
@@ -81,7 +97,8 @@ class SearchSelectWidget(forms.Widget):
return [value] if value not in (None, "") else []
def render(self, name, value, attrs=None, renderer=None):
selected = searchselect_selected(self._values(value), _game_options)
selected = searchselect_selected(self._values(value), self.options_resolver)
autofocus = bool((attrs or {}).get("autofocus"))
return SearchSelect(
name=name,
selected=selected,
@@ -93,6 +110,7 @@ class SearchSelectWidget(forms.Widget):
always_visible=self.always_visible,
placeholder=self.placeholder,
id=(attrs or {}).get("id", ""),
autofocus=autofocus,
)
def value_from_datadict(self, data, files, name):
@@ -109,7 +127,9 @@ class SearchSelectMultiple(SearchSelectWidget):
class SessionForm(forms.ModelForm):
game = SingleGameChoiceField(
queryset=Game.objects.order_by("sort_name"),
widget=SearchSelectWidget(search_url="/api/games/search"),
widget=SearchSelectWidget(
search_url="/api/games/search", options_resolver=_game_options
),
)
duration_manual = forms.DurationField(
@@ -120,7 +140,11 @@ class SessionForm(forms.ModelForm):
label="Manual duration",
)
device = forms.ModelChoiceField(
queryset=Device.objects.order_by("name"), required=False
queryset=Device.objects.order_by("name"),
required=False,
widget=SearchSelectWidget(
search_url="/api/devices/search", options_resolver=_device_options
),
)
mark_as_played = forms.BooleanField(
@@ -191,9 +215,18 @@ class PurchaseForm(forms.ModelForm):
games = MultipleGameChoiceField(
queryset=Game.objects.order_by("sort_name"),
widget=SearchSelectMultiple(search_url="/api/games/search", multi_select=True),
widget=SearchSelectMultiple(
search_url="/api/games/search",
options_resolver=_game_options,
multi_select=True,
),
)
platform = forms.ModelChoiceField(
queryset=Platform.objects.order_by("name"),
widget=SearchSelectWidget(
search_url="/api/platforms/search", options_resolver=_platform_options
),
)
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
related_purchase = RelatedPurchaseChoiceField(
queryset=related_purchase_queryset(),
required=False,
@@ -270,7 +303,11 @@ class GameModelChoiceField(forms.ModelChoiceField):
class GameForm(forms.ModelForm):
platform = forms.ModelChoiceField(
queryset=Platform.objects.order_by("name"), required=False
queryset=Platform.objects.order_by("name"),
required=False,
widget=SearchSelectWidget(
search_url="/api/platforms/search", options_resolver=_platform_options
),
)
class Meta:
@@ -307,9 +344,13 @@ class DeviceForm(forms.ModelForm):
class PlayEventForm(forms.ModelForm):
game = GameModelChoiceField(
game = SingleGameChoiceField(
queryset=Game.objects.order_by("sort_name"),
widget=forms.Select(attrs={"autofocus": "autofocus"}),
widget=SearchSelectWidget(
search_url="/api/games/search",
options_resolver=_game_options,
attrs={"autofocus": "autofocus"},
),
)
mark_as_finished = forms.BooleanField(
+7 -2
View File
@@ -180,7 +180,7 @@ def add_game(request: HttpRequest) -> HttpResponse:
),
),
title="Add New Game",
scripts=ModuleScript("add_game.js"),
scripts=ModuleScript("search_select.js") + ModuleScript("add_game.js"),
)
@@ -332,7 +332,12 @@ def edit_game(request: HttpRequest, game_id: int) -> HttpResponse:
if form.is_valid():
form.save()
return redirect("games:list_sessions")
return render_page(request, AddForm(form, request=request), title="Edit Game")
return render_page(
request,
AddForm(form, request=request),
title="Edit Game",
scripts=ModuleScript("search_select.js"),
)
# --- view_game content builders -------------------------------------------
+11 -2
View File
@@ -15,6 +15,7 @@ from common.components import (
Button,
ButtonGroup,
Icon,
ModuleScript,
paginated_table_content,
)
from common.layout import render_page
@@ -193,7 +194,10 @@ def add_playevent(request: HttpRequest, game_id: int = 0) -> HttpResponse:
return HttpResponseRedirect(reverse("games:view_game", args=[game_id]))
return render_page(
request, AddForm(form, request=request), title="Add new playthrough"
request,
AddForm(form, request=request),
title="Add new playthrough",
scripts=ModuleScript("search_select.js"),
)
@@ -206,7 +210,12 @@ def edit_playevent(request: HttpRequest, playevent_id: int) -> HttpResponse:
reverse("games:view_game", args=[playevent.game.id])
)
return render_page(request, AddForm(form, request=request), title="Edit Play Event")
return render_page(
request,
AddForm(form, request=request),
title="Edit Play Event",
scripts=ModuleScript("search_select.js"),
)
def delete_playevent(request: HttpRequest, playevent_id: int) -> HttpResponse: