Rename Button to StyledButton, simplify A

This commit is contained in:
2026-06-14 10:47:23 +02:00
parent 7751c29529
commit f036a246a8
11 changed files with 82 additions and 111 deletions
+2 -2
View File
@@ -48,7 +48,6 @@ from common.components.primitives import (
H1, H1,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
Checkbox, Checkbox,
CsrfInput, CsrfInput,
@@ -68,6 +67,7 @@ from common.components.primitives import (
SimpleTable, SimpleTable,
Span, Span,
StaticScript, StaticScript,
StyledButton,
TableHeader, TableHeader,
TableRow, TableRow,
TableTd, TableTd,
@@ -109,7 +109,7 @@ __all__ = [
"randomid", "randomid",
"A", "A",
"AddForm", "AddForm",
"Button", "StyledButton",
"ButtonGroup", "ButtonGroup",
"Checkbox", "Checkbox",
"CsrfInput", "CsrfInput",
+4 -31
View File
@@ -9,7 +9,6 @@ Everything returns a :class:`Node`; string-built widgets return :class:`Safe`.
from django.middleware.csrf import get_token from django.middleware.csrf import get_token
from django.templatetags.static import static from django.templatetags.static import static
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
@@ -98,6 +97,8 @@ def _html_element(tag_name: str, media: Media | None = None):
return element return element
A = _html_element("a")
Button = _html_element("button")
Div = _html_element("div") Div = _html_element("div")
P = _html_element("p") P = _html_element("p")
Ul = _html_element("ul") Ul = _html_element("ul")
@@ -218,35 +219,7 @@ def PopoverTruncated(
return input_string return input_string
def A( def StyledButton(
attributes: Attributes | None = None,
children: Children = None,
url_name: str | None = None,
href: str | None = None,
) -> Element:
"""
Returns an anchor <a> 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(
attributes: Attributes | None = None, attributes: Attributes | None = None,
children: Children = None, children: Children = None,
size: str = "base", size: str = "base",
@@ -683,7 +656,7 @@ def AddForm(
children=[ children=[
CsrfInput(request), CsrfInput(request),
field_markup, field_markup,
Div(children=[Button(submit_attrs, "Submit", type="submit")]), Div(children=[StyledButton(submit_attrs, "Submit", type="submit")]),
Div( Div(
[("class", "submit-button-container")], [("class", "submit-button-container")],
[additional_row] if additional_row else [], [additional_row] if additional_row else [],
+7 -5
View File
@@ -4,14 +4,14 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from common.components import ( from common.components import (
Fragment,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
Icon,
paginated_table_content,
DeviceFilterBar, DeviceFilterBar,
Fragment,
Icon,
StyledButton,
paginated_table_content,
) )
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
@@ -34,7 +34,9 @@ def list_devices(request: HttpRequest) -> HttpResponse:
devices, page_obj, elided_page_range = paginate(request, devices) devices, page_obj, elided_page_range = paginate(request, devices)
data = { data = {
"header_action": A([], Button([], "Add device"), url_name="games:add_device"), "header_action": A(href=reverse("games:add_device"))[
StyledButton()["Add device"]
],
"columns": [ "columns": [
"Name", "Name",
"Type", "Type",
+22 -26
View File
@@ -11,16 +11,15 @@ from django.urls import reverse
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from common.components import ( from common.components import (
Fragment,
H1, H1,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
CsrfInput, CsrfInput,
Div, Div,
Element, Element,
FilterBar, FilterBar,
Fragment,
GameStatus, GameStatus,
GameStatusSelector, GameStatusSelector,
Icon, Icon,
@@ -35,6 +34,7 @@ from common.components import (
Safe, Safe,
SearchField, SearchField,
SimpleTable, SimpleTable,
StyledButton,
Ul, Ul,
paginated_table_content, paginated_table_content,
) )
@@ -90,12 +90,11 @@ def list_games(request: HttpRequest, search_string: str = "") -> HttpResponse:
data = { data = {
"header_action": Div( "header_action": Div(
children=[ class_="flex justify-between",
)[
SearchField(search_string=search_string), SearchField(search_string=search_string),
A([], Button([], "Add game"), url_name="games:add_game"), A(href=reverse("games:add_game"))[StyledButton()["Add game"]],
], ],
attributes=[("class", "flex justify-between")],
),
"columns": [ "columns": [
"Name", "Name",
"Sort Name", "Sort Name",
@@ -172,7 +171,7 @@ def add_game(request: HttpRequest) -> HttpResponse:
AddForm( AddForm(
form, form,
request=request, request=request,
additional_row=Button( additional_row=StyledButton(
[], [],
"Submit & Create Purchase", "Submit & Create Purchase",
color="gray", color="gray",
@@ -248,14 +247,14 @@ def _delete_game_confirmation_modal(
Div( Div(
[("class", "items-center mt-5")], [("class", "items-center mt-5")],
[ [
Button( StyledButton(
[("class", "w-full")], [("class", "w-full")],
"Delete", "Delete",
color="red", color="red",
size="lg", size="lg",
type="submit", type="submit",
), ),
Button( StyledButton(
[("class", "mt-0 w-full")], [("class", "mt-0 w-full")],
"Cancel", "Cancel",
color="gray", color="gray",
@@ -353,26 +352,26 @@ _PLAYED_MENU = (
def _played_row(game: Game, request: HttpRequest) -> Node: def _played_row(game: Game, request: HttpRequest) -> Node:
"""'Played N times' control as a custom element (ts/elements/play-event-row.ts).""" """'Played N times' control as a custom element (ts/elements/play-event-row.ts)."""
from common.components import Element, custom_element from common.components import Element
from common.components.custom_elements import PlayEventRowProps, _PlayEventRow from common.components.custom_elements import _PlayEventRow
from common.components.primitives import Button
played: int = 0
played = game.playevents.count() played = game.playevents.count()
count_button = A(href=reverse("games:add_playevent"))[ count_button = A(href=reverse("games:add_playevent"))[
Element( Button(class_=_PLAYED_BTN + " rounded-s-lg")[
"button", Span(data_count="")[str(played)], " times"
[("type", "button"), ("class", _PLAYED_BTN + " rounded-s-lg")], ]
[Span(data_count="")[str(played)], " times"],
)
] ]
menu = Div(data_menu="", hidden=True, class_=_PLAYED_MENU)[ menu = Div(data_menu="", hidden=True, class_=_PLAYED_MENU)[
Ul()[ 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]))[ A(href=reverse("games:add_playevent_for_game", args=[game.id]))[
"Add playthrough..." "Add playthrough..."
] ]
], ],
Li(attributes=[("class", "px-4 py-2 cursor-pointer")])[ Li(class_="px-4 py-2 cursor-pointer")[
Element( Element(
"button", "button",
[("type", "button"), ("data-add-play", "")], [("type", "button"), ("data-add-play", "")],
@@ -401,9 +400,7 @@ def _played_row(game: Game, request: HttpRequest) -> Node:
game_id=game.id, game_id=game.id,
csrf=get_token(request), csrf=get_token(request),
api_create_url=reverse("api-1.0.0:create_playevent"), api_create_url=reverse("api-1.0.0:create_playevent"),
)[Div(class_="flex gap-2 items-center")[ )[Div(class_="flex gap-2 items-center")[Span(class_="uppercase")["Played"], group]]
Span(class_="uppercase")["Played"], group
]]
def _stat_popover(popover_id: str, tooltip: str, svg_key: str, value: str) -> SafeText: 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( header_action = Div(
children=[ children=[
A( A(href=reverse("games:add_session"))[
url_name="games:add_session", StyledButton(icon=True, color="blue", size="xs")[Icon("plus")]
children=Button(icon=True, size="xs", children=[Icon("play"), "LOG"]), ],
),
A( A(
href=reverse( href=reverse(
"games:list_sessions_start_session_from_session", "games:list_sessions_start_session_from_session",
@@ -699,7 +695,7 @@ def _sessions_section(game: Game, request: HttpRequest) -> SafeText:
children=Popover( children=Popover(
popover_content=last_session.game.name, popover_content=last_session.game.name,
children=[ children=[
Button( StyledButton(
icon=True, icon=True,
color="gray", color="gray",
size="xs", size="xs",
+6 -6
View File
@@ -4,14 +4,14 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from common.components import ( from common.components import (
Fragment,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
Fragment,
Icon, Icon,
paginated_table_content,
PlatformFilterBar, PlatformFilterBar,
StyledButton,
paginated_table_content,
) )
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
@@ -35,9 +35,9 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
platforms, page_obj, elided_page_range = paginate(request, platforms) platforms, page_obj, elided_page_range = paginate(request, platforms)
data = { data = {
"header_action": A( "header_action": A(href=reverse("games:add_platform"))[
[], Button([], "Add platform"), url_name="games:add_platform" StyledButton()["Add platform"]
), ],
"columns": [ "columns": [
"Name", "Name",
"Icon", "Icon",
+6 -7
View File
@@ -9,17 +9,16 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from common.components import ( from common.components import (
Fragment,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
Fragment,
Icon, Icon,
ModuleScript, ModuleScript,
paginated_table_content,
PlayEventFilterBar, PlayEventFilterBar,
StyledButton,
paginated_table_content,
) )
from common.layout import render_page from common.layout import render_page
from common.time import dateformat, format_duration, local_strftime from common.time import dateformat, format_duration, local_strftime
@@ -87,9 +86,9 @@ def create_playevent_tabledata(
for row in row_list for row in row_list
] ]
return { return {
"header_action": A( "header_action": A(href=reverse("games:add_playevent"))[
[], Button([], "Add play event"), url_name="games:add_playevent" StyledButton()["Add play event"]
), ],
"columns": list(filtered_column_list), "columns": list(filtered_column_list),
"rows": filtered_row_list, "rows": filtered_row_list,
} }
+8 -8
View File
@@ -14,14 +14,13 @@ from django.utils.safestring import SafeText, mark_safe
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from common.components import ( from common.components import (
Fragment,
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
CsrfInput, CsrfInput,
Div, Div,
Element, Element,
Fragment,
GameLink, GameLink,
Icon, Icon,
LinkedPurchase, LinkedPurchase,
@@ -30,6 +29,7 @@ from common.components import (
Node, Node,
PriceConverted, PriceConverted,
PurchasePrice, PurchasePrice,
StyledButton,
TableRow, TableRow,
paginated_table_content, paginated_table_content,
) )
@@ -110,9 +110,9 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
purchases, page_obj, elided_page_range = paginate(request, purchases) purchases, page_obj, elided_page_range = paginate(request, purchases)
data = { data = {
"header_action": A( "header_action": A(href=reverse("games:add_purchase"))[
[], Button([], "Add purchase"), url_name="games:add_purchase" StyledButton()["Add purchase"]
), ],
"columns": [ "columns": [
"Name", "Name",
"Type", "Type",
@@ -153,7 +153,7 @@ def _purchase_additional_row() -> SafeText:
Td(), Td(),
Td( Td(
children=[ children=[
Button( StyledButton(
[], [],
"Submit & Create Session", "Submit & Create Session",
color="gray", color="gray",
@@ -319,14 +319,14 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> Node:
Div( Div(
[("class", "items-center mt-5")], [("class", "items-center mt-5")],
[ [
Button( StyledButton(
[("class", "w-full")], [("class", "w-full")],
"Refund", "Refund",
color="blue", color="blue",
size="lg", size="lg",
type="submit", type="submit",
), ),
Button( StyledButton(
[("class", "mt-0 w-full")], [("class", "mt-0 w-full")],
"Cancel", "Cancel",
color="gray", color="gray",
+11 -11
View File
@@ -13,7 +13,6 @@ from django.utils.safestring import SafeText, mark_safe
from common.components import ( from common.components import (
A, A,
AddForm, AddForm,
Button,
ButtonGroup, ButtonGroup,
Div, Div,
Fragment, Fragment,
@@ -25,9 +24,10 @@ from common.components import (
Safe, Safe,
SearchField, SearchField,
SessionDeviceSelector, SessionDeviceSelector,
SessionTimestampButtons,
StyledButton,
paginated_table_content, paginated_table_content,
) )
from common.components import SessionTimestampButtons
from common.components.primitives import Span, Td, Tr 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 (
@@ -77,13 +77,13 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
Div( Div(
children=[ children=[
A( A(
url_name="games:add_session", href=reverse("games:add_session"),
children=Button( )[
StyledButton(
icon=True, icon=True,
size="xs", size="xs",
children=[Icon("play"), "LOG"], )[Icon("play"), "LOG"]
), ],
),
A( A(
href=reverse( href=reverse(
"games:list_sessions_start_session_from_session", "games:list_sessions_start_session_from_session",
@@ -92,7 +92,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
children=Popover( children=Popover(
popover_content=last_session.game.name, popover_content=last_session.game.name,
children=[ children=[
Button( StyledButton(
icon=True, icon=True,
color="gray", color="gray",
size="xs", size="xs",
@@ -213,13 +213,13 @@ def _session_fields(form) -> Fragment:
class_="form-row-button-group flex-row gap-3 justify-start mt-3", class_="form-row-button-group flex-row gap-3 justify-start mt-3",
hx_boost="false", 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" "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" "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}" f"Copy {this_side} value to {other_side}"
], ],
] ]
+4 -4
View File
@@ -9,6 +9,7 @@ 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.urls import reverse
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from common.components import ( from common.components import (
A, A,
Div, Div,
@@ -100,10 +101,9 @@ def _year_nav(year, year_range, url_template) -> Node:
else "text-body hover:text-heading underline decoration-dotted" else "text-body hover:text-heading underline decoration-dotted"
) )
alltime_btn = A( alltime_btn = A(
url_name="games:stats_alltime", href=reverse("games:stats_alltime"),
attributes=[("class", alltime_classes)], class_=alltime_classes,
children=["All-time stats"], )["All-time stats"]
)
picker = YearPicker( picker = YearPicker(
year=year_int, year=year_int,
available_years=tuple(year_range or []), available_years=tuple(year_range or []),
+3 -3
View File
@@ -7,10 +7,10 @@ from django.utils.safestring import SafeText
from common.components import ( from common.components import (
A, A,
AddForm, AddForm,
Button,
CsrfInput, CsrfInput,
Div, Div,
Element, Element,
StyledButton,
paginated_table_content, paginated_table_content,
) )
from common.components.primitives import P from common.components.primitives import P
@@ -79,12 +79,12 @@ def _delete_statuschange_content(statuschange, request: HttpRequest) -> SafeText
P( P(
children=["Are you sure you want to delete this status change?"], children=["Are you sure you want to delete this status change?"],
), ),
Button( StyledButton(
[("class", "w-full")], "Delete", color="red", type="submit", size="lg" [("class", "w-full")], "Delete", color="red", type="submit", size="lg"
), ),
A( A(
[("class", "")], [("class", "")],
Button([("class", "w-full")], "Cancel", color="gray"), StyledButton([("class", "w-full")], "Cancel", color="gray"),
href=reverse("games:view_game", args=[statuschange.game.id]), href=reverse("games:view_game", args=[statuschange.game.id]),
), ),
], ],
+7 -6
View File
@@ -6,7 +6,7 @@ from django.test import SimpleTestCase
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import SafeText, mark_safe
from common import components 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 # Component builders return lazy ``Node`` objects; these tests assert on rendered
# HTML, so node-returning calls are wrapped in ``str(...)`` at the call site # 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")) str(components.A(href="/path", url_name="some_name"))
def test_button_returns_safe_text(self): def test_button_returns_safe_text(self):
result = str(components.Button([], "click")) result = str(components.StyledButton([], "click"))
self.assertIsInstance(result, SafeText) self.assertIsInstance(result, SafeText)
self.assertIn("<button", result) self.assertIn("<button", result)
def test_button_default_colors(self): def test_button_default_colors(self):
result = str(components.Button([], "click")) result = str(components.StyledButton([], "click"))
self.assertIn("text-white bg-brand", result) self.assertIn("text-white bg-brand", result)
def test_name_with_icon_no_link(self): def test_name_with_icon_no_link(self):
@@ -269,7 +269,7 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
def test_component_output_starts_with_tag(self): def test_component_output_starts_with_tag(self):
for label, html in [ for label, html in [
("A", str(components.A(href="/foo", children=["link"]))), ("A", str(components.A(href="/foo", children=["link"]))),
("Button", str(components.Button([], "click"))), ("Button", str(components.StyledButton([], "click"))),
("Div", str(components.Div([], ["hello"]))), ("Div", str(components.Div([], ["hello"]))),
("Input", str(components.Input())), ("Input", str(components.Input())),
("ButtonGroup", str(components.ButtonGroup([]))), ("ButtonGroup", str(components.ButtonGroup([]))),
@@ -294,7 +294,7 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
def test_button_with_icon_children_not_escaped(self): def test_button_with_icon_children_not_escaped(self):
result = str( result = str(
components.Button( components.StyledButton(
icon=True, icon=True,
size="xs", size="xs",
children=[components.Icon("play"), "LOG"], children=[components.Icon("play"), "LOG"],
@@ -307,7 +307,7 @@ class ComponentOutputIsNotEscapedTest(unittest.TestCase):
components.Popover( components.Popover(
popover_content="test tooltip", popover_content="test tooltip",
children=[ children=[
components.Button( components.StyledButton(
icon=True, icon=True,
color="gray", color="gray",
size="xs", size="xs",
@@ -923,6 +923,7 @@ class ComponentPrimitivesTest(SimpleTestCase):
class PrimitiveWidgetsTest(SimpleTestCase): class PrimitiveWidgetsTest(SimpleTestCase):
def test_mixin_applies_widget_to_boolean_fields_only(self): def test_mixin_applies_widget_to_boolean_fields_only(self):
from django import forms from django import forms
from games.forms import PrimitiveCheckboxWidget, PrimitiveWidgetsMixin from games.forms import PrimitiveCheckboxWidget, PrimitiveWidgetsMixin
class DummyForm(PrimitiveWidgetsMixin, forms.Form): class DummyForm(PrimitiveWidgetsMixin, forms.Form):