Use adhoc Component() less

This commit is contained in:
2026-06-09 18:00:57 +02:00
parent 0179363684
commit 32eb882a98
9 changed files with 247 additions and 242 deletions
+43 -40
View File
@@ -4,8 +4,6 @@ Split into core / primitives / domain / filters submodules; this package
re-exports the public API so ``from common.components import X`` keeps working. re-exports the public API so ``from common.components import X`` keeps working.
""" """
from common.utils import truncate
from common.components.core import ( from common.components.core import (
Component, Component,
HTMLAttribute, HTMLAttribute,
@@ -13,41 +11,6 @@ from common.components.core import (
_render_element, _render_element,
randomid, randomid,
) )
from common.components.primitives import (
A,
AddForm,
Button,
ButtonGroup,
CsrfInput,
Div,
ExternalScript,
H1,
Icon,
Input,
Modal,
ModuleScript,
Pill,
Popover,
PopoverTruncated,
SearchField,
SimpleTable,
Span,
Label,
TableHeader,
TableRow,
TableTd,
Template,
YearPicker,
paginated_table_content,
)
from common.components.search_select import (
DEFAULT_PREFETCH,
FilterSelect,
LabeledOption,
SearchSelect,
SearchSelectOption,
searchselect_selected,
)
from common.components.domain import ( from common.components.domain import (
GameLink, GameLink,
GameStatus, GameStatus,
@@ -60,13 +23,53 @@ from common.components.domain import (
_resolve_name_with_icon, _resolve_name_with_icon,
) )
from common.components.filters import ( from common.components.filters import (
FilterBar,
PurchaseFilterBar,
SessionFilterBar,
DeviceFilterBar, DeviceFilterBar,
FilterBar,
PlatformFilterBar, PlatformFilterBar,
PlayEventFilterBar, PlayEventFilterBar,
PurchaseFilterBar,
SessionFilterBar,
) )
from common.components.primitives import (
H1,
A,
AddForm,
Button,
ButtonGroup,
CsrfInput,
Div,
ExternalScript,
Icon,
Input,
Label,
Li,
Modal,
ModuleScript,
Pill,
Popover,
PopoverTruncated,
SearchField,
SimpleTable,
Span,
TableHeader,
TableRow,
TableTd,
Td,
Template,
Tr,
Ul,
YearPicker,
paginated_table_content,
)
from common.components.search_select import (
DEFAULT_PREFETCH,
FilterSelect,
LabeledOption,
SearchSelect,
SearchSelectOption,
searchselect_selected,
)
from common.utils import truncate
__all__ = [ __all__ = [
"truncate", "truncate",
+3 -4
View File
@@ -6,7 +6,7 @@ from django.template.defaultfilters import floatformat
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.components.core import Component, HTMLTag from common.components.core import HTMLTag
from common.components.primitives import ( from common.components.primitives import (
A, A,
Div, Div,
@@ -33,10 +33,9 @@ def GameLink(
return Span( return Span(
attributes=[("class", "truncate-container")], attributes=[("class", "truncate-container")],
children=[ children=[
Component( A(
tag_name="a", href=link,
attributes=[ attributes=[
("href", link),
("class", "underline decoration-slate-500 sm:decoration-2"), ("class", "underline decoration-slate-500 sm:decoration-2"),
], ],
children=display if isinstance(display, list) else [display], children=display if isinstance(display, list) else [display],
+59 -72
View File
@@ -6,8 +6,12 @@ from django.db import models
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.components.core import Component from common.components.core import Component
from common.components.primitives import Label, Span from common.components.primitives import Div, Input, Label, Span
from common.components.search_select import DEFAULT_PREFETCH, FilterSelect, LabeledOption from common.components.search_select import (
DEFAULT_PREFETCH,
FilterSelect,
LabeledOption,
)
class FilterChoice(NamedTuple): class FilterChoice(NamedTuple):
@@ -206,8 +210,7 @@ def _filter_mins_to_hrs(val) -> str:
def _filter_field(label: str, widget) -> SafeText: def _filter_field(label: str, widget) -> SafeText:
"""A labelled filter field: <div><label>…</label>{widget}</div>.""" """A labelled filter field: <div><label>…</label>{widget}</div>."""
return Component( return Div(
tag_name="div",
attributes=[("class", "flex flex-col gap-1")], attributes=[("class", "flex flex-col gap-1")],
children=[ children=[
Label( Label(
@@ -223,8 +226,7 @@ def _filter_checkbox(name: str, label: str, checked: bool) -> SafeText:
return Label( return Label(
attributes=[("class", "flex items-center gap-2 text-sm text-heading")], attributes=[("class", "flex items-center gap-2 text-sm text-heading")],
children=[ children=[
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "checkbox"), ("type", "checkbox"),
("name", name), ("name", name),
@@ -283,13 +285,11 @@ def RangeSlider(
point_mode = bool(min_value and max_value and min_value == max_value) point_mode = bool(min_value and max_value and min_value == max_value)
initial_mode = "point" if point_mode else "range" initial_mode = "point" if point_mode else "range"
return Component( return Div(
tag_name="div",
attributes=[("class", "range-slider-block mb-4")], attributes=[("class", "range-slider-block mb-4")],
children=[ children=[
# ── Label row ── # ── Label row ──
Component( Div(
tag_name="div",
attributes=[("class", "flex items-center gap-2 mb-1")], attributes=[("class", "flex items-center gap-2 mb-1")],
children=[ children=[
Label( Label(
@@ -299,8 +299,7 @@ def RangeSlider(
], ],
children=[label], children=[label],
), ),
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "number"), ("type", "number"),
("name", min_input_id), ("name", min_input_id),
@@ -324,8 +323,7 @@ def RangeSlider(
], ],
children=[""], children=[""],
), ),
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "number"), ("type", "number"),
("name", max_input_id), ("name", max_input_id),
@@ -379,8 +377,7 @@ def RangeSlider(
], ],
), ),
# ── Slider row ── # ── Slider row ──
Component( Div(
tag_name="div",
attributes=[ attributes=[
("class", "range-slider relative h-10 select-none mt-1"), ("class", "range-slider relative h-10 select-none mt-1"),
("data-mode", initial_mode), ("data-mode", initial_mode),
@@ -389,8 +386,7 @@ def RangeSlider(
("data-step", str(step)), ("data-step", str(step)),
], ],
children=[ children=[
Component( Div(
tag_name="div",
attributes=[ attributes=[
( (
"class", "class",
@@ -399,8 +395,7 @@ def RangeSlider(
), ),
], ],
), ),
Component( Div(
tag_name="div",
attributes=[ attributes=[
( (
"class", "class",
@@ -411,8 +406,7 @@ def RangeSlider(
], ],
), ),
# Min handle (hidden in point mode via JS) # Min handle (hidden in point mode via JS)
Component( Div(
tag_name="div",
attributes=[ attributes=[
( (
"class", "class",
@@ -429,8 +423,7 @@ def RangeSlider(
], ],
), ),
# Max handle # Max handle
Component( Div(
tag_name="div",
attributes=[ attributes=[
( (
"class", "class",
@@ -480,8 +473,7 @@ def _filter_collapse_button() -> SafeText:
def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText: def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText:
return Component( return Div(
tag_name="div",
attributes=[("class", "flex gap-3 items-center")], attributes=[("class", "flex gap-3 items-center")],
children=[ children=[
Component( Component(
@@ -521,8 +513,7 @@ def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText:
("id", "save-preset-area"), ("id", "save-preset-area"),
], ],
children=[ children=[
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "text"), ("type", "text"),
("id", "preset-name-input"), ("id", "preset-name-input"),
@@ -572,8 +563,7 @@ def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText:
), ),
], ],
), ),
Component( Div(
tag_name="div",
attributes=[ attributes=[
("id", "preset-dropdown"), ("id", "preset-dropdown"),
("class", "relative"), ("class", "relative"),
@@ -594,13 +584,11 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
"""Shared collapsible filter-bar chrome. `fields` is the per-entity body """Shared collapsible filter-bar chrome. `fields` is the per-entity body
(grids, sliders, checkboxes); the shell adds the collapse toggle, the form, (grids, sliders, checkboxes); the shell adds the collapse toggle, the form,
the hidden filter-json input and the Apply/Clear/preset action row.""" the hidden filter-json input and the Apply/Clear/preset action row."""
return Component( return Div(
tag_name="div",
attributes=[("id", "filter-bar"), ("class", "mb-6")], attributes=[("id", "filter-bar"), ("class", "mb-6")],
children=[ children=[
_filter_collapse_button(), _filter_collapse_button(),
Component( Div(
tag_name="div",
attributes=[ attributes=[
("id", "filter-bar-body"), ("id", "filter-bar-body"),
( (
@@ -617,8 +605,7 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe
("onsubmit", "return applyFilterBar(event)"), ("onsubmit", "return applyFilterBar(event)"),
], ],
children=[ children=[
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "hidden"), ("type", "hidden"),
("id", _FILTER_INPUT_ID), ("id", _FILTER_INPUT_ID),
@@ -725,8 +712,7 @@ def FilterBar(
price_range_max = max(int(price_aggregate.get("price_max") or 100), 1) price_range_max = max(int(price_aggregate.get("price_max") or 100), 1)
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
@@ -817,8 +803,7 @@ def FilterBar(
min_placeholder="e.g. 1985", min_placeholder="e.g. 1985",
max_placeholder="e.g. 2010", max_placeholder="e.g. 2010",
), ),
Component( Div(
tag_name="div",
attributes=[("class", "flex items-end gap-4 mb-4 flex-wrap")], attributes=[("class", "flex items-end gap-4 mb-4 flex-wrap")],
children=[ children=[
_filter_checkbox("filter-mastered", "Mastered", mastered_value), _filter_checkbox("filter-mastered", "Mastered", mastered_value),
@@ -970,8 +955,7 @@ def SessionFilterBar(
duration_range_max = 200 duration_range_max = 200
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
@@ -1027,8 +1011,7 @@ def SessionFilterBar(
min_placeholder="e.g. 30", min_placeholder="e.g. 30",
max_placeholder="e.g. 180", max_placeholder="e.g. 180",
), ),
Component( Div(
tag_name="div",
attributes=[("class", "flex gap-4 mb-4")], attributes=[("class", "flex gap-4 mb-4")],
children=[ children=[
_filter_checkbox("filter-emulated", "Emulated", emulated_value), _filter_checkbox("filter-emulated", "Emulated", emulated_value),
@@ -1079,8 +1062,7 @@ def PurchaseFilterBar(
num_range_min, num_range_max = 0, 10 num_range_min, num_range_max = 0, 10
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
@@ -1127,42 +1109,48 @@ def PurchaseFilterBar(
), ),
], ],
), ),
Component( Div(
tag_name="div",
attributes=[("class", "flex items-end gap-4 mb-4")], attributes=[("class", "flex items-end gap-4 mb-4")],
children=[ children=[
_filter_checkbox("filter-refunded", "Refunded", is_refunded_value), _filter_checkbox("filter-refunded", "Refunded", is_refunded_value),
_filter_checkbox("filter-infinite", "Infinite", infinite_value), _filter_checkbox("filter-infinite", "Infinite", infinite_value),
_filter_checkbox("filter-needs-price-update", "Needs Price Update", needs_price_update_value), _filter_checkbox(
"filter-needs-price-update",
"Needs Price Update",
needs_price_update_value,
),
], ],
), ),
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
"Original Currency", "Original Currency",
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "text"), ("type", "text"),
("name", "filter-price_currency"), ("name", "filter-price_currency"),
("value", price_currency_value), ("value", price_currency_value),
("placeholder", "e.g. USD, EUR"), ("placeholder", "e.g. USD, EUR"),
("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), (
"class",
"w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body",
),
], ],
), ),
), ),
_filter_field( _filter_field(
"Converted Currency", "Converted Currency",
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "text"), ("type", "text"),
("name", "filter-converted_currency"), ("name", "filter-converted_currency"),
("value", converted_currency_value), ("value", converted_currency_value),
("placeholder", "e.g. USD, EUR"), ("placeholder", "e.g. USD, EUR"),
("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), (
"class",
"w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body",
),
], ],
), ),
), ),
@@ -1193,9 +1181,7 @@ def PurchaseFilterBar(
return _filter_bar(fields, filter_json, preset_list_url, preset_save_url) return _filter_bar(fields, filter_json, preset_list_url, preset_save_url)
def DeviceFilterBar( def DeviceFilterBar(filter_json="", preset_list_url="", preset_save_url="") -> SafeText:
filter_json="", preset_list_url="", preset_save_url=""
) -> SafeText:
"""Collapsible filter bar for the Device list.""" """Collapsible filter bar for the Device list."""
from games.models import Device from games.models import Device
@@ -1204,8 +1190,7 @@ def DeviceFilterBar(
type_choice = _filter_get_choice(existing, "type") type_choice = _filter_get_choice(existing, "type")
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
@@ -1233,33 +1218,36 @@ def PlatformFilterBar(
group_value = existing.get("group", {}).get("value", "") group_value = existing.get("group", {}).get("value", "")
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
"Platform Name", "Platform Name",
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "text"), ("type", "text"),
("name", "filter-name"), ("name", "filter-name"),
("value", name_value), ("value", name_value),
("placeholder", "e.g. Nintendo Switch"), ("placeholder", "e.g. Nintendo Switch"),
("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), (
"class",
"w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body",
),
], ],
), ),
), ),
_filter_field( _filter_field(
"Platform Group", "Platform Group",
Component( Input(
tag_name="input",
attributes=[ attributes=[
("type", "text"), ("type", "text"),
("name", "filter-group"), ("name", "filter-group"),
("value", group_value), ("value", group_value),
("placeholder", "e.g. Nintendo"), ("placeholder", "e.g. Nintendo"),
("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), (
"class",
"w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body",
),
], ],
), ),
), ),
@@ -1278,8 +1266,7 @@ def PlayEventFilterBar(
days_min, days_max = _parse_range(existing, "days_to_finish") days_min, days_max = _parse_range(existing, "days_to_finish")
fields = [ fields = [
Component( Div(
tag_name="div",
attributes=[("class", _FILTER_GRID_CLASS)], attributes=[("class", _FILTER_GRID_CLASS)],
children=[ children=[
_filter_field( _filter_field(
+76 -23
View File
@@ -6,10 +6,9 @@ from django.urls import reverse
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.components.core import Component, HTMLAttribute, HTMLTag, randomid
from common.icons import get_icon from common.icons import get_icon
from common.utils import truncate from common.utils import truncate
from common.components.core import Component, HTMLAttribute, HTMLTag, randomid
_COLOR_CLASSES = { _COLOR_CLASSES = {
"blue": "text-white bg-brand box-border border border-transparent hover:bg-brand-strong focus:ring-4 focus:ring-brand-medium", "blue": "text-white bg-brand box-border border border-transparent hover:bg-brand-strong focus:ring-4 focus:ring-brand-medium",
@@ -57,8 +56,7 @@ def _popover_html(
"dark:bg-purple-800" "dark:bg-purple-800"
) )
div = Component( div = Div(
tag_name="div",
attributes=[ attributes=[
("data-popover", ""), ("data-popover", ""),
("id", id), ("id", id),
@@ -66,12 +64,11 @@ def _popover_html(
("class", popover_tooltip_class), ("class", popover_tooltip_class),
], ],
children=[ children=[
Component( Div(
tag_name="div",
attributes=[("class", "px-3 py-2")], attributes=[("class", "px-3 py-2")],
children=[popover_content], children=[popover_content],
), ),
Component(tag_name="div", attributes=[("data-popper-arrow", "")]), Div(attributes=[("data-popper-arrow", "")]),
mark_safe( # nosec — intentional HTML comment for Tailwind JIT mark_safe( # nosec — intentional HTML comment for Tailwind JIT
"<!-- for Tailwind CSS to generate decoration-dotted CSS " "<!-- for Tailwind CSS to generate decoration-dotted CSS "
"from Python component -->" "from Python component -->"
@@ -323,8 +320,7 @@ def ButtonGroup(buttons: list[dict] | None = None) -> SafeText:
) )
) )
return Component( return Div(
tag_name="div",
attributes=[("class", "inline-flex rounded-md shadow-xs"), ("role", "group")], attributes=[("class", "inline-flex rounded-md shadow-xs"), ("role", "group")],
children=children, children=children,
) )
@@ -339,6 +335,42 @@ def Div(
return Component(tag_name="div", attributes=attributes, children=children) return Component(tag_name="div", attributes=attributes, children=children)
def P(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="p", attributes=attributes, children=children)
def Ul(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="ul", attributes=attributes, children=children)
def Li(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="li", attributes=attributes, children=children)
def Strong(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="strong", attributes=attributes, children=children)
def Input( def Input(
type: str = "text", type: str = "text",
attributes: list[HTMLAttribute] | None = None, attributes: list[HTMLAttribute] | None = None,
@@ -600,8 +632,7 @@ def SearchField(
], ],
children=["Search"], children=["Search"],
), ),
Component( Div(
tag_name="div",
attributes=[("class", "relative")], attributes=[("class", "relative")],
children=[ children=[
mark_safe( mark_safe(
@@ -612,10 +643,9 @@ def SearchField(
'd="m21 21-3.5-3.5M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/>' 'd="m21 21-3.5-3.5M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/>'
"</svg></div>" "</svg></div>"
), ),
Component( Input(
tag_name="input", type="search",
attributes=[ attributes=[
("type", "search"),
("id", id), ("id", id),
("name", id), ("name", id),
("value", search_string), ("value", search_string),
@@ -687,8 +717,7 @@ def Modal(
) -> SafeText: ) -> SafeText:
"""Modal overlay with container. Content (form, buttons) goes in children.""" """Modal overlay with container. Content (form, buttons) goes in children."""
children = children or [] children = children or []
outer = Component( outer = Div(
tag_name="div",
attributes=[ attributes=[
("id", modal_id), ("id", modal_id),
( (
@@ -698,8 +727,7 @@ def Modal(
), ),
], ],
children=[ children=[
Component( Div(
tag_name="div",
attributes=[ attributes=[
( (
"class", "class",
@@ -714,13 +742,39 @@ def Modal(
return mark_safe(str(outer)) return mark_safe(str(outer))
def Td(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="td", attributes=attributes, children=children)
def Tr(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="tr", attributes=attributes, children=children)
def Th(
attributes: list[HTMLAttribute] | None = None,
children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText:
attributes = attributes or []
children = children or []
return Component(tag_name="th", attributes=attributes, children=children)
def TableTd( def TableTd(
children: list[HTMLTag] | HTMLTag | None = None, children: list[HTMLTag] | HTMLTag | None = None,
) -> SafeText: ) -> SafeText:
"""Styled table cell.""" """Styled table cell."""
children = children or [] children = children or []
return Component( return Td(
tag_name="td",
attributes=[("class", "px-6 py-4 min-w-20-char max-w-20-char")], attributes=[("class", "px-6 py-4 min-w-20-char max-w-20-char")],
children=children if isinstance(children, list) else [children], children=children if isinstance(children, list) else [children],
) )
@@ -765,8 +819,7 @@ def TableRow(data: dict | list | None = None) -> SafeText:
for i, cell in enumerate(cells): for i, cell in enumerate(cells):
if i == 0: if i == 0:
cell_elements.append( cell_elements.append(
Component( Th(
tag_name="th",
attributes=[ attributes=[
("scope", "row"), ("scope", "row"),
( (
@@ -781,7 +834,7 @@ def TableRow(data: dict | list | None = None) -> SafeText:
else: else:
cell_elements.append(TableTd(children=[cell])) cell_elements.append(TableTd(children=[cell]))
return Component(tag_name="tr", attributes=tr_attrs, children=cell_elements) return Tr(attributes=tr_attrs, children=cell_elements)
def Icon( def Icon(
+4 -5
View File
@@ -6,6 +6,7 @@ from django.http import HttpResponse
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.components import Component, CsrfInput, Div, Input from common.components import Component, CsrfInput, Div, Input
from common.components.primitives import Td, Tr
from common.layout import render_page from common.layout import render_page
@@ -15,12 +16,10 @@ def _login_content(form, request) -> SafeText:
children=[ children=[
CsrfInput(request), CsrfInput(request),
mark_safe(str(form.as_table())), mark_safe(str(form.as_table())),
Component( Tr(
tag_name="tr",
children=[ children=[
Component(tag_name="td"), Td(),
Component( Td(
tag_name="td",
children=[ children=[
Input(type="submit", attributes=[("value", "Login")]) Input(type="submit", attributes=[("value", "Login")])
], ],
+30 -53
View File
@@ -2,15 +2,16 @@ from typing import Any
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.middleware.csrf import get_token
from django.db.models import Q from django.db.models import Q
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.middleware.csrf import get_token
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import date as date_filter
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common.components import ( from common.components import (
H1,
A, A,
AddForm, AddForm,
Button, Button,
@@ -21,9 +22,7 @@ from common.components import (
FilterBar, FilterBar,
GameStatus, GameStatus,
GameStatusSelector, GameStatusSelector,
H1,
Icon, Icon,
SearchField,
LinkedPurchase, LinkedPurchase,
Modal, Modal,
ModuleScript, ModuleScript,
@@ -31,9 +30,12 @@ from common.components import (
Popover, Popover,
PopoverTruncated, PopoverTruncated,
PurchasePrice, PurchasePrice,
SearchField,
SimpleTable, SimpleTable,
Ul,
paginated_table_content, paginated_table_content,
) )
from common.components.primitives import Li, Span, Strong
from common.icons import get_icon from common.icons import get_icon
from common.layout import render_page from common.layout import render_page
from common.time import ( from common.time import (
@@ -193,19 +195,13 @@ def _delete_game_confirmation_modal(
) -> SafeText: ) -> SafeText:
data_items = [] data_items = []
if session_count: if session_count:
data_items.append( data_items.append(Li(children=[f"{session_count} session(s)"]))
Component(tag_name="li", children=[f"{session_count} session(s)"])
)
if purchase_count: if purchase_count:
data_items.append( data_items.append(Li(children=[f"{purchase_count} purchase(s)"]))
Component(tag_name="li", children=[f"{purchase_count} purchase(s)"])
)
if playevent_count: if playevent_count:
data_items.append( data_items.append(Li(children=[f"{playevent_count} play event(s)"]))
Component(tag_name="li", children=[f"{playevent_count} play event(s)"])
)
if not (session_count or purchase_count or playevent_count): if not (session_count or purchase_count or playevent_count):
data_items.append(Component(tag_name="li", children=["No associated data"])) data_items.append(Li(children=["No associated data"]))
form = Component( form = Component(
tag_name="form", tag_name="form",
@@ -218,8 +214,7 @@ def _delete_game_confirmation_modal(
], ],
children=[ children=[
CsrfInput(request), CsrfInput(request),
Component( P(
tag_name="p",
attributes=[ attributes=[
( (
"class", "class",
@@ -231,8 +226,7 @@ def _delete_game_confirmation_modal(
"This will permanently delete this game and all associated data:" "This will permanently delete this game and all associated data:"
], ],
), ),
Component( Ul(
tag_name="ul",
attributes=[ attributes=[
( (
"class", "class",
@@ -242,8 +236,7 @@ def _delete_game_confirmation_modal(
], ],
children=data_items, children=data_items,
), ),
Component( P(
tag_name="p",
attributes=[ attributes=[
( (
"class", "class",
@@ -279,8 +272,7 @@ def _delete_game_confirmation_modal(
return Modal( return Modal(
"delete-game-confirmation-modal", "delete-game-confirmation-modal",
children=[ children=[
Component( P(
tag_name="h1",
attributes=[ attributes=[
( (
"class", "class",
@@ -289,12 +281,11 @@ def _delete_game_confirmation_modal(
], ],
children=["Delete Game"], children=["Delete Game"],
), ),
Component( P(
tag_name="p",
attributes=[("class", "dark:text-white text-center mt-5")], attributes=[("class", "dark:text-white text-center mt-5")],
children=[ children=[
"Are you sure you want to delete ", "Are you sure you want to delete ",
Component(tag_name="strong", children=[game.name]), Strong(children=[game.name]),
"?", "?",
], ],
), ),
@@ -427,9 +418,7 @@ def _meta_row(
label: str, value: SafeText | str, extra: SafeText | str = "" label: str, value: SafeText | str, extra: SafeText | str = ""
) -> SafeText: ) -> SafeText:
children: list[SafeText | str] = [ children: list[SafeText | str] = [
Component( Span(attributes=[("class", "uppercase")], children=[label]),
tag_name="span", attributes=[("class", "uppercase")], children=[label]
),
value, value,
] ]
if extra: if extra:
@@ -452,9 +441,8 @@ def _game_action_buttons(game: Game) -> SafeText:
"dark:text-white dark:hover:text-white dark:hover:bg-red-700 " "dark:text-white dark:hover:text-white dark:hover:bg-red-700 "
"dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer" "dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer"
) )
edit_link = Component( edit_link = A(
tag_name="a", href=reverse("games:edit_game", args=[game.id]),
attributes=[("href", reverse("games:edit_game", args=[game.id]))],
children=[ children=[
Component( Component(
tag_name="button", tag_name="button",
@@ -463,10 +451,9 @@ def _game_action_buttons(game: Game) -> SafeText:
) )
], ],
) )
delete_link = Component( delete_link = A(
tag_name="a", href="#",
attributes=[ attributes=[
("href", "#"),
("hx-get", reverse("games:delete_game_confirmation", args=[game.id])), ("hx-get", reverse("games:delete_game_confirmation", args=[game.id])),
("hx-target", "#global-modal-container"), ("hx-target", "#global-modal-container"),
], ],
@@ -499,21 +486,16 @@ def _game_history(statuschanges) -> SafeText:
status=change.new_status, status=change.new_status,
children=[change.get_new_status_display()], children=[change.get_new_status_display()],
) )
edit = Component( edit = A(
tag_name="a", href=reverse("games:edit_statuschange", args=[change.id]),
attributes=[("href", reverse("games:edit_statuschange", args=[change.id]))],
children=["Edit"], children=["Edit"],
) )
delete = Component( delete = A(
tag_name="a", href=reverse("games:delete_statuschange", args=[change.id]),
attributes=[
("href", reverse("games:delete_statuschange", args=[change.id]))
],
children=["Delete"], children=["Delete"],
) )
items.append( items.append(
Component( Li(
tag_name="li",
attributes=[("class", "text-slate-500")], attributes=[("class", "text-slate-500")],
children=[ children=[
f"{prefix} status from ", f"{prefix} status from ",
@@ -528,8 +510,7 @@ def _game_history(statuschanges) -> SafeText:
], ],
) )
) )
return Component( return Ul(
tag_name="ul",
attributes=[("class", "list-disc list-inside")], attributes=[("class", "list-disc list-inside")],
children=items, children=items,
) )
@@ -576,12 +557,10 @@ def _game_overview_metrics(game: Game) -> dict[str, Any]:
def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> SafeText: def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> SafeText:
grey_value_class = "text-black dark:text-slate-300" grey_value_class = "text-black dark:text-slate-300"
title_span = Component( title_span = Span(
tag_name="span",
attributes=[("class", "text-balance max-w-120 text-4xl")], attributes=[("class", "text-balance max-w-120 text-4xl")],
children=[ children=[
Component( Span(
tag_name="span",
attributes=[("class", "font-bold font-serif")], attributes=[("class", "font-bold font-serif")],
children=[game.name], children=[game.name],
), ),
@@ -634,8 +613,7 @@ def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> S
[ [
_meta_row( _meta_row(
"Original year", "Original year",
Component( Span(
tag_name="span",
attributes=[("class", grey_value_class)], attributes=[("class", grey_value_class)],
children=[str(game.original_year_released)], children=[str(game.original_year_released)],
), ),
@@ -648,8 +626,7 @@ def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> S
_played_row(game, request), _played_row(game, request),
_meta_row( _meta_row(
"Platform", "Platform",
Component( Span(
tag_name="span",
attributes=[("class", grey_value_class)], attributes=[("class", grey_value_class)],
children=[str(game.platform)], children=[str(game.platform)],
), ),
+17 -21
View File
@@ -6,13 +6,12 @@ from django.http import (
HttpResponseRedirect, HttpResponseRedirect,
) )
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_POST
from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import date as date_filter
from django.template.defaultfilters import floatformat from django.template.defaultfilters import floatformat
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from django.views.decorators.http import require_POST
from common.components import ( from common.components import (
A, A,
@@ -32,6 +31,7 @@ from common.components import (
TableRow, TableRow,
paginated_table_content, paginated_table_content,
) )
from common.components.primitives import Li, P, Td, Tr, Ul
from common.layout import render_page from common.layout import render_page
from common.time import dateformat from common.time import dateformat
from common.utils import paginate from common.utils import paginate
@@ -129,7 +129,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
elided_page_range=elided_page_range, elided_page_range=elided_page_range,
request=request, request=request,
) )
from common.components import PurchaseFilterBar, ModuleScript from common.components import ModuleScript, PurchaseFilterBar
filter_bar = PurchaseFilterBar( filter_bar = PurchaseFilterBar(
filter_json=filter_json, filter_json=filter_json,
@@ -149,12 +149,10 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
def _purchase_additional_row() -> SafeText: def _purchase_additional_row() -> SafeText:
"""The 'Submit & Create Session' row shown below the main Submit button.""" """The 'Submit & Create Session' row shown below the main Submit button."""
return Component( return Tr(
tag_name="tr",
children=[ children=[
Component(tag_name="td"), Td(),
Component( Td(
tag_name="td",
children=[ children=[
Button( Button(
[], [],
@@ -262,8 +260,7 @@ def _view_purchase_content(purchase: Purchase) -> SafeText:
Div( Div(
[("class", row_class)], [("class", row_class)],
[ [
Component( P(
tag_name="p",
children=[ children=[
"Price per game: ", "Price per game: ",
PriceConverted([floatformat(purchase.price_per_game, 0)]), PriceConverted([floatformat(purchase.price_per_game, 0)]),
@@ -273,10 +270,9 @@ def _view_purchase_content(purchase: Purchase) -> SafeText:
], ],
), ),
Div([("class", row_class)], ["Games included in this purchase:"]), Div([("class", row_class)], ["Games included in this purchase:"]),
Component( Ul(
tag_name="ul",
children=[ children=[
Component(tag_name="li", children=[GameLink(game.id, game.name)]) Li(children=[GameLink(game.id, game.name)])
for game in purchase.games.all() for game in purchase.games.all()
], ],
), ),
@@ -317,8 +313,7 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> SafeTe
], ],
children=[ children=[
CsrfInput(request), CsrfInput(request),
Component( P(
tag_name="p",
attributes=[("class", "dark:text-white text-center mt-3 text-sm")], attributes=[("class", "dark:text-white text-center mt-3 text-sm")],
children=["Games will be marked as abandoned."], children=["Games will be marked as abandoned."],
), ),
@@ -356,8 +351,7 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> SafeTe
], ],
children=["Confirm Refund"], children=["Confirm Refund"],
), ),
Component( P(
tag_name="p",
attributes=[("class", "dark:text-white text-center mt-5")], attributes=[("class", "dark:text-white text-center mt-5")],
children=["Are you sure you want to mark this purchase as refunded?"], children=["Are you sure you want to mark this purchase as refunded?"],
), ),
@@ -408,8 +402,10 @@ def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
from games.forms import related_purchase_queryset from games.forms import related_purchase_queryset
form = PurchaseForm() form = PurchaseForm()
qs = related_purchase_queryset().filter(games__in=games).order_by( qs = (
"games__sort_name" related_purchase_queryset()
.filter(games__in=games)
.order_by("games__sort_name")
) )
form.fields["related_purchase"].queryset = qs form.fields["related_purchase"].queryset = qs
+13 -22
View File
@@ -15,7 +15,6 @@ from common.components import (
AddForm, AddForm,
Button, Button,
ButtonGroup, ButtonGroup,
Component,
Div, Div,
Icon, Icon,
ModuleScript, ModuleScript,
@@ -25,6 +24,7 @@ from common.components import (
SessionDeviceSelector, SessionDeviceSelector,
paginated_table_content, paginated_table_content,
) )
from common.components.primitives import Span, Td, Tr
from common.layout import render_page from common.layout import render_page
from common.time import ( from common.time import (
dateformat, dateformat,
@@ -208,8 +208,7 @@ def _session_fields(form) -> SafeText:
this_side = "start" if field.name == "timestamp_start" else "end" this_side = "start" if field.name == "timestamp_start" else "end"
other_side = "end" if field.name == "timestamp_start" else "start" other_side = "end" if field.name == "timestamp_start" else "start"
children.append( children.append(
Component( Span(
tag_name="span",
attributes=[ attributes=[
( (
"class", "class",
@@ -292,8 +291,8 @@ def edit_session(request: HttpRequest, session_id: int) -> HttpResponse:
def _session_row_fragment(session: Session) -> SafeText: def _session_row_fragment(session: Session) -> SafeText:
"""A single session <tr> (the old list_sessions.html#session-row partial), """A single session <tr> (the old list_sessions.html#session-row partial),
returned by the inline end/clone-session HTMX endpoints.""" returned by the inline end/clone-session HTMX endpoints."""
name_link = Component( name_link = A(
tag_name="a", href=reverse("games:view_game", args=[session.game.id]),
attributes=[ attributes=[
( (
"class", "class",
@@ -305,12 +304,10 @@ def _session_row_fragment(session: Session) -> SafeText:
"group-hover:outline-purple-400 group-hover:outline-4 " "group-hover:outline-purple-400 group-hover:outline-4 "
"group-hover:decoration-purple-900 group-hover:text-purple-100", "group-hover:decoration-purple-900 group-hover:text-purple-100",
), ),
("href", reverse("games:view_game", args=[session.game.id])),
], ],
children=[session.game.name], children=[session.game.name],
) )
name_td = Component( name_td = Td(
tag_name="td",
attributes=[ attributes=[
( (
"class", "class",
@@ -319,15 +316,13 @@ def _session_row_fragment(session: Session) -> SafeText:
) )
], ],
children=[ children=[
Component( Span(
tag_name="span",
attributes=[("class", "inline-block relative")], attributes=[("class", "inline-block relative")],
children=[name_link], children=[name_link],
) )
], ],
) )
start_td = Component( start_td = Td(
tag_name="td",
attributes=[ attributes=[
("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell") ("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell")
], ],
@@ -336,10 +331,9 @@ def _session_row_fragment(session: Session) -> SafeText:
if not session.timestamp_end: if not session.timestamp_end:
end_url = reverse("games:list_sessions_end_session", args=[session.id]) end_url = reverse("games:list_sessions_end_session", args=[session.id])
end_inner: SafeText | str = Component( end_inner: SafeText | str = A(
tag_name="a", href=end_url,
attributes=[ attributes=[
("href", end_url),
("hx-get", end_url), ("hx-get", end_url),
("hx-target", "closest tr"), ("hx-target", "closest tr"),
("hx-swap", "outerHTML"), ("hx-swap", "outerHTML"),
@@ -351,8 +345,7 @@ def _session_row_fragment(session: Session) -> SafeText:
), ),
], ],
children=[ children=[
Component( Span(
tag_name="span",
attributes=[("class", "text-yellow-300")], attributes=[("class", "text-yellow-300")],
children=["Finish now?"], children=["Finish now?"],
) )
@@ -362,19 +355,17 @@ def _session_row_fragment(session: Session) -> SafeText:
end_inner = "--" end_inner = "--"
else: else:
end_inner = date_filter(session.timestamp_end, "d/m/Y H:i") end_inner = date_filter(session.timestamp_end, "d/m/Y H:i")
end_td = Component( end_td = Td(
tag_name="td",
attributes=[ attributes=[
("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell") ("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell")
], ],
children=[end_inner], children=[end_inner],
) )
duration_td = Component( duration_td = Td(
tag_name="td",
attributes=[("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono")], attributes=[("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono")],
children=[session.duration_formatted()], children=[session.duration_formatted()],
) )
return Component(tag_name="tr", children=[name_td, start_td, end_td, duration_td]) return Tr(children=[name_td, start_td, end_td, duration_td])
def clone_session_by_id(session_id: int) -> Session: def clone_session_by_id(session_id: int) -> Session:
+2 -2
View File
@@ -13,6 +13,7 @@ from common.components import (
Div, Div,
paginated_table_content, paginated_table_content,
) )
from common.components.primitives import P
from common.layout import render_page from common.layout import render_page
from common.time import dateformat, local_strftime from common.time import dateformat, local_strftime
from common.utils import paginate from common.utils import paginate
@@ -75,8 +76,7 @@ def _delete_statuschange_content(statuschange, request: HttpRequest) -> SafeText
inner = Div( inner = Div(
[], [],
[ [
Component( P(
tag_name="p",
children=["Are you sure you want to delete this status change?"], children=["Are you sure you want to delete this status change?"],
), ),
Button( Button(