Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion django_admin_react/api/views/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

from typing import Any

from django.contrib.admin.utils import model_format_dict
from django.db import transaction
from django.http import HttpRequest
from django.http import HttpResponse
Expand Down Expand Up @@ -138,9 +139,22 @@ def actions_payload(model_admin: Any, request: HttpRequest) -> list[dict[str, An
confirmation step regardless — this hint is a UX optimisation.
"""
raw = model_admin.get_actions(request) or {}
# Django's built-in `delete_selected` (and any action whose
# `short_description` uses the admin's `%(verbose_name)s` /
# `%(verbose_name_plural)s` placeholders) ships a *format string*,
# not a finished label — Django interpolates it at render time via
# `model_format_dict(opts)`. Do the same here so the SPA shows
# "Delete selected files", never the raw "%(verbose_name_plural)s".
fmt = model_format_dict(model_admin.model._meta)
out: list[dict[str, Any]] = []
for name, (_callable, _resolved_name, description) in raw.items():
label = str(description) if description else name
raw_label = str(description) if description else name
try:
label = raw_label % fmt
except (KeyError, ValueError, TypeError):
# Not a %-format string, or references a key we don't
# provide — surface the label verbatim rather than crashing.
label = raw_label
requires_conf = "delete" in (label.lower() + " " + name.lower())
out.append(
{
Expand Down
13 changes: 13 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ def test_actions_include_default_delete(superuser_client: Client) -> None:
assert action["requires_confirmation"] is True


@pytest.mark.django_db
def test_delete_selected_label_is_interpolated(superuser_client: Client) -> None:
"""``delete_selected``'s ``%(verbose_name_plural)s`` placeholder is
interpolated with the model's plural — never shown raw to the SPA."""
response = superuser_client.get(LIST_URL)
delete = next(a for a in response.json()["actions"] if a["name"] == "delete_selected")
# The raw Django short_description is "Delete selected
# %(verbose_name_plural)s"; the SPA must receive the finished label.
assert "%(" not in delete["label"]
assert "verbose_name_plural" not in delete["label"]
assert "users" in delete["label"].lower() # auth.User → "users"


# --------------------------------------------------------------------------- #
# §6 mandatory matrix on the runner #
# --------------------------------------------------------------------------- #
Expand Down
Loading