From f036a246a8dec87862d92fbee76b14460fa28b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Sun, 14 Jun 2026 10:47:23 +0200 Subject: [PATCH] Rename Button to StyledButton, simplify A --- common/components/__init__.py | 4 +-- common/components/primitives.py | 35 +++------------------- games/views/device.py | 12 ++++---- games/views/game.py | 52 +++++++++++++++------------------ games/views/platform.py | 12 ++++---- games/views/playevent.py | 13 ++++----- games/views/purchase.py | 16 +++++----- games/views/session.py | 22 +++++++------- games/views/stats_content.py | 8 ++--- games/views/statuschange.py | 6 ++-- tests/test_components.py | 13 +++++---- 11 files changed, 82 insertions(+), 111 deletions(-) diff --git a/common/components/__init__.py b/common/components/__init__.py index 10e4bfb..a97c062 100644 --- a/common/components/__init__.py +++ b/common/components/__init__.py @@ -48,7 +48,6 @@ from common.components.primitives import ( H1, A, AddForm, - Button, ButtonGroup, Checkbox, CsrfInput, @@ -68,6 +67,7 @@ from common.components.primitives import ( SimpleTable, Span, StaticScript, + StyledButton, TableHeader, TableRow, TableTd, @@ -109,7 +109,7 @@ __all__ = [ "randomid", "A", "AddForm", - "Button", + "StyledButton", "ButtonGroup", "Checkbox", "CsrfInput", diff --git a/common/components/primitives.py b/common/components/primitives.py index 808e5e9..ef1ec21 100644 --- a/common/components/primitives.py +++ b/common/components/primitives.py @@ -9,7 +9,6 @@ Everything returns a :class:`Node`; string-built widgets return :class:`Safe`. from django.middleware.csrf import get_token from django.templatetags.static import static -from django.urls import reverse from django.utils.html import conditional_escape from django.utils.safestring import SafeText, mark_safe @@ -98,6 +97,8 @@ def _html_element(tag_name: str, media: Media | None = None): return element +A = _html_element("a") +Button = _html_element("button") Div = _html_element("div") P = _html_element("p") Ul = _html_element("ul") @@ -218,35 +219,7 @@ def PopoverTruncated( return input_string -def A( - attributes: Attributes | None = None, - children: Children = None, - url_name: str | None = None, - href: str | None = None, -) -> Element: - """ - Returns an anchor tag. - - Accepts one of two mutually-exclusive URL specifications: - - url_name: URL pattern name, resolved via reverse() - - href: Literal path string passed through as-is - """ - attributes = as_attributes(attributes) - children = children or [] - if url_name is not None and href is not None: - raise ValueError("Provide exactly one of 'url_name' or 'href', not both.") - - additional_attributes = [] - if url_name is not None: - additional_attributes = [("href", reverse(url_name))] - elif href is not None: - additional_attributes = [("href", href)] - return Element( - "a", attributes=attributes + additional_attributes, children=children - ) - - -def Button( +def StyledButton( attributes: Attributes | None = None, children: Children = None, size: str = "base", @@ -683,7 +656,7 @@ def AddForm( children=[ CsrfInput(request), field_markup, - Div(children=[Button(submit_attrs, "Submit", type="submit")]), + Div(children=[StyledButton(submit_attrs, "Submit", type="submit")]), Div( [("class", "submit-button-container")], [additional_row] if additional_row else [], diff --git a/games/views/device.py b/games/views/device.py index 08d0a94..640203b 100644 --- a/games/views/device.py +++ b/games/views/device.py @@ -4,14 +4,14 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from common.components import ( - Fragment, A, AddForm, - Button, ButtonGroup, - Icon, - paginated_table_content, DeviceFilterBar, + Fragment, + Icon, + StyledButton, + paginated_table_content, ) from common.layout import render_page from common.time import dateformat, local_strftime @@ -34,7 +34,9 @@ def list_devices(request: HttpRequest) -> HttpResponse: devices, page_obj, elided_page_range = paginate(request, devices) data = { - "header_action": A([], Button([], "Add device"), url_name="games:add_device"), + "header_action": A(href=reverse("games:add_device"))[ + StyledButton()["Add device"] + ], "columns": [ "Name", "Type", diff --git a/games/views/game.py b/games/views/game.py index 82a5917..9a72075 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -11,16 +11,15 @@ from django.urls import reverse from django.utils.safestring import SafeText from common.components import ( - Fragment, H1, A, AddForm, - Button, ButtonGroup, CsrfInput, Div, Element, FilterBar, + Fragment, GameStatus, GameStatusSelector, Icon, @@ -35,6 +34,7 @@ from common.components import ( Safe, SearchField, SimpleTable, + StyledButton, Ul, paginated_table_content, ) @@ -90,12 +90,11 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse: data = { "header_action": Div( - children=[ - SearchField(search_string=search_string), - A([], Button([], "Add game"), url_name="games:add_game"), - ], - attributes=[("class", "flex justify-between")], - ), + class_="flex justify-between", + )[ + SearchField(search_string=search_string), + A(href=reverse("games:add_game"))[StyledButton()["Add game"]], + ], "columns": [ "Name", "Sort Name", @@ -172,7 +171,7 @@ def add_game(request: HttpRequest) -> HttpResponse: AddForm( form, request=request, - additional_row=Button( + additional_row=StyledButton( [], "Submit & Create Purchase", color="gray", @@ -248,14 +247,14 @@ def _delete_game_confirmation_modal( Div( [("class", "items-center mt-5")], [ - Button( + StyledButton( [("class", "w-full")], "Delete", color="red", size="lg", type="submit", ), - Button( + StyledButton( [("class", "mt-0 w-full")], "Cancel", color="gray", @@ -353,26 +352,26 @@ _PLAYED_MENU = ( def _played_row(game: Game, request: HttpRequest) -> Node: """'Played N times' control as a custom element (ts/elements/play-event-row.ts).""" - from common.components import Element, custom_element - from common.components.custom_elements import PlayEventRowProps, _PlayEventRow + from common.components import Element + from common.components.custom_elements import _PlayEventRow + from common.components.primitives import Button + played: int = 0 played = game.playevents.count() count_button = A(href=reverse("games:add_playevent"))[ - Element( - "button", - [("type", "button"), ("class", _PLAYED_BTN + " rounded-s-lg")], - [Span(data_count="")[str(played)], " times"], - ) + Button(class_=_PLAYED_BTN + " rounded-s-lg")[ + Span(data_count="")[str(played)], " times" + ] ] menu = Div(data_menu="", hidden=True, class_=_PLAYED_MENU)[ Ul()[ - Li(attributes=[("class", "px-4 py-2")])[ + Li(class_="px-4 py-2")[ A(href=reverse("games:add_playevent_for_game", args=[game.id]))[ "Add playthrough..." ] ], - Li(attributes=[("class", "px-4 py-2 cursor-pointer")])[ + Li(class_="px-4 py-2 cursor-pointer")[ Element( "button", [("type", "button"), ("data-add-play", "")], @@ -401,9 +400,7 @@ def _played_row(game: Game, request: HttpRequest) -> Node: game_id=game.id, csrf=get_token(request), api_create_url=reverse("api-1.0.0:create_playevent"), - )[Div(class_="flex gap-2 items-center")[ - Span(class_="uppercase")["Played"], group - ]] + )[Div(class_="flex gap-2 items-center")[Span(class_="uppercase")["Played"], group]] def _stat_popover(popover_id: str, tooltip: str, svg_key: str, value: str) -> SafeText: @@ -687,10 +684,9 @@ def _sessions_section(game: Game, request: HttpRequest) -> SafeText: header_action = Div( children=[ - A( - url_name="games:add_session", - children=Button(icon=True, size="xs", children=[Icon("play"), "LOG"]), - ), + A(href=reverse("games:add_session"))[ + StyledButton(icon=True, color="blue", size="xs")[Icon("plus")] + ], A( href=reverse( "games:list_sessions_start_session_from_session", @@ -699,7 +695,7 @@ def _sessions_section(game: Game, request: HttpRequest) -> SafeText: children=Popover( popover_content=last_session.game.name, children=[ - Button( + StyledButton( icon=True, color="gray", size="xs", diff --git a/games/views/platform.py b/games/views/platform.py index 2a86b33..4cc720a 100644 --- a/games/views/platform.py +++ b/games/views/platform.py @@ -4,14 +4,14 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from common.components import ( - Fragment, A, AddForm, - Button, ButtonGroup, + Fragment, Icon, - paginated_table_content, PlatformFilterBar, + StyledButton, + paginated_table_content, ) from common.layout import render_page from common.time import dateformat, local_strftime @@ -35,9 +35,9 @@ def list_platforms(request: HttpRequest) -> HttpResponse: platforms, page_obj, elided_page_range = paginate(request, platforms) data = { - "header_action": A( - [], Button([], "Add platform"), url_name="games:add_platform" - ), + "header_action": A(href=reverse("games:add_platform"))[ + StyledButton()["Add platform"] + ], "columns": [ "Name", "Icon", diff --git a/games/views/playevent.py b/games/views/playevent.py index d1efe58..2dd792c 100644 --- a/games/views/playevent.py +++ b/games/views/playevent.py @@ -9,17 +9,16 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse - from common.components import ( - Fragment, A, AddForm, - Button, ButtonGroup, + Fragment, Icon, ModuleScript, - paginated_table_content, PlayEventFilterBar, + StyledButton, + paginated_table_content, ) from common.layout import render_page from common.time import dateformat, format_duration, local_strftime @@ -87,9 +86,9 @@ def create_playevent_tabledata( for row in row_list ] return { - "header_action": A( - [], Button([], "Add play event"), url_name="games:add_playevent" - ), + "header_action": A(href=reverse("games:add_playevent"))[ + StyledButton()["Add play event"] + ], "columns": list(filtered_column_list), "rows": filtered_row_list, } diff --git a/games/views/purchase.py b/games/views/purchase.py index 2f4d0db..6b3c4a1 100644 --- a/games/views/purchase.py +++ b/games/views/purchase.py @@ -14,14 +14,13 @@ from django.utils.safestring import SafeText, mark_safe from django.views.decorators.http import require_POST from common.components import ( - Fragment, A, AddForm, - Button, ButtonGroup, CsrfInput, Div, Element, + Fragment, GameLink, Icon, LinkedPurchase, @@ -30,6 +29,7 @@ from common.components import ( Node, PriceConverted, PurchasePrice, + StyledButton, TableRow, paginated_table_content, ) @@ -110,9 +110,9 @@ def list_purchases(request: HttpRequest) -> HttpResponse: purchases, page_obj, elided_page_range = paginate(request, purchases) data = { - "header_action": A( - [], Button([], "Add purchase"), url_name="games:add_purchase" - ), + "header_action": A(href=reverse("games:add_purchase"))[ + StyledButton()["Add purchase"] + ], "columns": [ "Name", "Type", @@ -153,7 +153,7 @@ def _purchase_additional_row() -> SafeText: Td(), Td( children=[ - Button( + StyledButton( [], "Submit & Create Session", color="gray", @@ -319,14 +319,14 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> Node: Div( [("class", "items-center mt-5")], [ - Button( + StyledButton( [("class", "w-full")], "Refund", color="blue", size="lg", type="submit", ), - Button( + StyledButton( [("class", "mt-0 w-full")], "Cancel", color="gray", diff --git a/games/views/session.py b/games/views/session.py index 64d140b..2b5321b 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -13,7 +13,6 @@ from django.utils.safestring import SafeText, mark_safe from common.components import ( A, AddForm, - Button, ButtonGroup, Div, Fragment, @@ -25,9 +24,10 @@ from common.components import ( Safe, SearchField, SessionDeviceSelector, + SessionTimestampButtons, + StyledButton, paginated_table_content, ) -from common.components import SessionTimestampButtons from common.components.primitives import Span, Td, Tr from common.layout import render_page from common.time import ( @@ -77,13 +77,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse Div( children=[ A( - url_name="games:add_session", - children=Button( + href=reverse("games:add_session"), + )[ + StyledButton( icon=True, size="xs", - children=[Icon("play"), "LOG"], - ), - ), + )[Icon("play"), "LOG"] + ], A( href=reverse( "games:list_sessions_start_session_from_session", @@ -92,7 +92,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse children=Popover( popover_content=last_session.game.name, children=[ - Button( + StyledButton( icon=True, color="gray", size="xs", @@ -213,13 +213,13 @@ def _session_fields(form) -> Fragment: class_="form-row-button-group flex-row gap-3 justify-start mt-3", hx_boost="false", )[ - Button(data_target=field.name, data_type="now", size="xs")[ + StyledButton(data_target=field.name, data_type="now", size="xs")[ "Set to now" ], - Button(data_target=field.name, data_type="toggle", size="xs")[ + StyledButton(data_target=field.name, data_type="toggle", size="xs")[ "Toggle text" ], - Button(data_target=field.name, data_type="copy", size="xs")[ + StyledButton(data_target=field.name, data_type="copy", size="xs")[ f"Copy {this_side} value to {other_side}" ], ] diff --git a/games/views/stats_content.py b/games/views/stats_content.py index b131b0b..dd84aec 100644 --- a/games/views/stats_content.py +++ b/games/views/stats_content.py @@ -9,6 +9,7 @@ from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import floatformat from django.urls import reverse from django.utils.html import conditional_escape + from common.components import ( A, Div, @@ -100,10 +101,9 @@ def _year_nav(year, year_range, url_template) -> Node: else "text-body hover:text-heading underline decoration-dotted" ) alltime_btn = A( - url_name="games:stats_alltime", - attributes=[("class", alltime_classes)], - children=["All-time stats"], - ) + href=reverse("games:stats_alltime"), + class_=alltime_classes, + )["All-time stats"] picker = YearPicker( year=year_int, available_years=tuple(year_range or []), diff --git a/games/views/statuschange.py b/games/views/statuschange.py index 651cc1f..317ee0d 100644 --- a/games/views/statuschange.py +++ b/games/views/statuschange.py @@ -7,10 +7,10 @@ from django.utils.safestring import SafeText from common.components import ( A, AddForm, - Button, CsrfInput, Div, Element, + StyledButton, paginated_table_content, ) from common.components.primitives import P @@ -79,12 +79,12 @@ def _delete_statuschange_content(statuschange, request: HttpRequest) -> SafeText P( children=["Are you sure you want to delete this status change?"], ), - Button( + StyledButton( [("class", "w-full")], "Delete", color="red", type="submit", size="lg" ), A( [("class", "")], - Button([("class", "w-full")], "Cancel", color="gray"), + StyledButton([("class", "w-full")], "Cancel", color="gray"), href=reverse("games:view_game", args=[statuschange.game.id]), ), ], diff --git a/tests/test_components.py b/tests/test_components.py index 64310b8..6cb9f72 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -6,7 +6,7 @@ from django.test import SimpleTestCase from django.utils.safestring import SafeText, mark_safe from common import components -from games.models import Platform, Game, Purchase, Session +from games.models import Game, Platform, Purchase, Session # Component builders return lazy ``Node`` objects; these tests assert on rendered # HTML, so node-returning calls are wrapped in ``str(...)`` at the call site @@ -243,12 +243,12 @@ class ComponentReturnTypeTest(unittest.TestCase): str(components.A(href="/path", url_name="some_name")) def test_button_returns_safe_text(self): - result = str(components.Button([], "click")) + result = str(components.StyledButton([], "click")) self.assertIsInstance(result, SafeText) self.assertIn("