from django import forms from django.db import transaction from django.db.models import OuterRef, Subquery from common.components import ( SearchSelect, SearchSelectOption, searchselect_selected, ) from games.models import ( Device, Game, GameStatusChange, Platform, PlayEvent, Purchase, Session, ) custom_date_widget = forms.DateInput(attrs={"type": "date"}) custom_datetime_widget = forms.DateTimeInput( attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M" ) autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"}) class MultipleGameChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj) -> str: return obj.search_label class SingleGameChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj) -> str: return obj.search_label def _game_options(values) -> list[SearchSelectOption]: """Resolve game ids (or instances) to SearchSelectOptions via one pk__in query.""" return [ { "value": g.id, "label": g.search_label, "data": {"platform": g.platform_id or ""}, } for g in Game.objects.filter(pk__in=values).select_related("platform") ] 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. The only place that knows about Django/forms — the component itself stays reusable outside forms. """ def __init__( self, *, search_url, options_resolver, multi_select=False, items_visible=5, items_scroll=10, always_visible=False, placeholder="Search…", attrs=None, ): 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 self.always_visible = always_visible self.placeholder = placeholder @staticmethod def _values(value) -> list: if value is None: return [] if isinstance(value, (list, tuple)): return [v for v in value if v not in (None, "")] return [value] if value not in (None, "") else [] def render(self, name, value, attrs=None, renderer=None): selected = searchselect_selected(self._values(value), self.options_resolver) autofocus = bool((attrs or {}).get("autofocus")) return SearchSelect( name=name, selected=selected, options=None, search_url=self.search_url, multi_select=self.multi_select, items_visible=self.items_visible, items_scroll=self.items_scroll, always_visible=self.always_visible, placeholder=self.placeholder, id=(attrs or {}).get("id", ""), autofocus=autofocus, ) def value_from_datadict(self, data, files, name): return data.get(name) class SearchSelectMultiple(SearchSelectWidget): def value_from_datadict(self, data, files, name): if hasattr(data, "getlist"): return data.getlist(name) return data.get(name) class SessionForm(forms.ModelForm): game = SingleGameChoiceField( queryset=Game.objects.order_by("sort_name"), widget=SearchSelectWidget( search_url="/api/games/search", options_resolver=_game_options ), ) duration_manual = forms.DurationField( required=False, widget=forms.TextInput( attrs={"x-mask": "99:99:99", "placeholder": "HH:MM:SS", "x-data": ""} ), label="Manual duration", ) device = forms.ModelChoiceField( queryset=Device.objects.order_by("name"), required=False, widget=SearchSelectWidget( search_url="/api/devices/search", options_resolver=_device_options ), ) mark_as_played = forms.BooleanField( required=False, initial={"mark_as_played": True}, label="Set game status to Played if Unplayed", ) class Meta: widgets = { "timestamp_start": custom_datetime_widget, "timestamp_end": custom_datetime_widget, } model = Session fields = [ "game", "timestamp_start", "timestamp_end", "duration_manual", "emulated", "device", "note", "mark_as_played", ] def save(self, commit=True): session = super().save(commit=False) if self.cleaned_data.get("mark_as_played"): game_instance = session.game if game_instance.status == "u": game_instance.status = "p" if commit: game_instance.save() if commit: session.save() return session def related_purchase_queryset(): """GAME purchases annotated with their first game's name. Rendering the ``related_purchase`` ``