From 59b91a8ec5664c1bf0b5bf025c6a94fb3521cd47 Mon Sep 17 00:00:00 2001 From: Antoliny0919 Date: Sun, 24 May 2026 22:21:38 +0900 Subject: [PATCH 1/2] Fixed #36192 -- Used semantic HTML for buttons. Co-authored-by: Phinart98 --- .../contrib/admin/static/admin/css/base.css | 7 +- .../admin/static/admin/css/changelists.css | 13 ++ .../contrib/admin/static/admin/css/forms.css | 8 +- .../admin/static/admin/css/widgets.css | 60 ++++-- .../contrib/admin/static/admin/js/actions.js | 4 +- .../admin/js/admin/DateTimeShortcuts.js | 194 +++++++++--------- .../contrib/admin/static/admin/js/calendar.js | 12 +- .../contrib/admin/static/admin/js/inlines.js | 32 +-- .../admin/templates/admin/actions.html | 4 +- .../templates/admin/change_list_results.html | 2 +- .../templates/admin/delete_confirmation.html | 2 +- .../admin/delete_selected_confirmation.html | 2 +- .../admin/templates/admin/submit_line.html | 4 +- .../contrib/admin/templatetags/admin_list.py | 4 +- .../auth/widgets/read_only_password_hash.html | 2 +- js_tests/admin/DateTimeShortcuts.test.js | 8 +- js_tests/admin/inlines.test.js | 20 +- tests/admin_changelist/tests.py | 4 +- tests/admin_inlines/tests.py | 61 ++++-- tests/admin_views/test_autocomplete_view.py | 4 +- tests/admin_views/tests.py | 32 ++- tests/admin_widgets/tests.py | 22 +- tests/auth_tests/test_forms.py | 10 +- tests/auth_tests/test_views.py | 6 +- 24 files changed, 287 insertions(+), 230 deletions(-) diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index 8f72023ea171..1c8050ca98cf 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -124,11 +124,14 @@ a:focus { } a:not( - [role="button"], #header a, #nav-sidebar a, #content-main.app-list a, - .object-tools a + .object-tools a, + .submit-row a, + .paginator a:not(.showall), + th .text a, + a.button ) { text-decoration: underline; } diff --git a/django/contrib/admin/static/admin/css/changelists.css b/django/contrib/admin/static/admin/css/changelists.css index d1bdb9f398ac..d97d5546347d 100644 --- a/django/contrib/admin/static/admin/css/changelists.css +++ b/django/contrib/admin/static/admin/css/changelists.css @@ -316,3 +316,16 @@ background-color: SelectedItem; } } + +#changelist .actions span.question button, +#changelist .actions span.clear button { + background: none; + border: none; + color: var(--link-fg); + cursor: pointer; +} + +#changelist .actions span.question button:hover, +#changelist .actions span.clear button:hover { + color: var(--link-hover-color); +} diff --git a/django/contrib/admin/static/admin/css/forms.css b/django/contrib/admin/static/admin/css/forms.css index f24feba73750..26379eefb34d 100644 --- a/django/contrib/admin/static/admin/css/forms.css +++ b/django/contrib/admin/static/admin/css/forms.css @@ -438,8 +438,12 @@ body.popup .submit-row { border-bottom: 1px solid var(--hairline-color); } -.inline-group div.add-row a, -.inline-group .tabular tr.add-row td a { +.inline-group div.add-row button, +.inline-group .tabular tr.add-row td button { + border: none; + padding-left: 16px; + cursor: pointer; + color: var(--link-fg); font-size: 0.75rem; } diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index 55d91035a3b3..fa597a6884e5 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -495,7 +495,8 @@ span.clearable-file-input label { border-bottom: none; } -.calendar td.selected a { +.calendar td.selected button, +.calendar td.selected button:hover { background: var(--secondary); color: var(--button-fg); } @@ -504,33 +505,32 @@ span.clearable-file-input label { background: var(--darkened-bg); } -.calendar td.today a { +.calendar td.today button { font-weight: 700; } -.calendar td a, -.timelist a { +.calendar td button, +.timelist button { display: block; + width: 100%; + height: 100%; + background: none; + border: none; + cursor: pointer; font-weight: 400; padding: 6px; text-decoration: none; color: var(--body-quiet-color); } -.calendar td a:focus, -.timelist a:focus, -.calendar td a:hover, -.timelist a:hover { +.calendar td button:focus, +.timelist button:focus, +.calendar td button:hover, +.timelist button:hover { background: var(--primary); color: white; } -.calendar td a:active, -.timelist a:active { - background: var(--header-bg); - color: white; -} - .calendarnav { font-size: 0.625rem; text-align: center; @@ -539,10 +539,7 @@ span.clearable-file-input label { padding: 1px 3px; } -.calendarnav a:link, -#calendarnav a:visited, -#calendarnav a:focus, -#calendarnav a:hover { +#calendarnav button:hover { color: var(--body-quiet-color); } @@ -559,6 +556,8 @@ span.clearable-file-input label { .calendarbox .calendarnav-next { display: block; position: absolute; + border: none; + cursor: pointer; top: 8px; width: 15px; height: 15px; @@ -590,9 +589,30 @@ span.clearable-file-input label { background: var(--close-button-hover-bg); } -.calendar-cancel a { - color: var(--button-fg); +.calendar-cancel button { display: block; + padding: 4px 0; + width: 100%; + height: 100%; + background: none; + border: none; + color: var(--button-fg); + cursor: pointer; +} + +.datetimeshortcuts button, +.calendar-shortcuts button { + background: none; + border: none; + cursor: pointer; + color: var(--link-fg); +} + +.datetimeshortcuts button:hover, +.datetimeshortcuts button:focus, +.calendar-shortcuts button:hover, +.calendar-shortcuts button:focus { + color: var(--link-hover-color); } ul.timelist, diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js index 879a46d8be40..2d2ef18c0838 100644 --- a/django/contrib/admin/static/admin/js/actions.js +++ b/django/contrib/admin/static/admin/js/actions.js @@ -121,7 +121,7 @@ }); document - .querySelectorAll(options.acrossQuestions + " a") + .querySelectorAll(options.acrossQuestions + " button") .forEach(function (el) { el.addEventListener("click", function (event) { event.preventDefault(); @@ -136,7 +136,7 @@ }); document - .querySelectorAll(options.acrossClears + " a") + .querySelectorAll(options.acrossClears + " button") .forEach(function (el) { el.addEventListener("click", function (event) { event.preventDefault(); diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 58a3523b009a..212886fb191f 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -21,9 +21,9 @@ dismissCalendarFunc: [], calendarDivName1: "calendarbox", // name of calendar
that gets toggled calendarDivName2: "calendarin", // name of
that contains calendar - calendarLinkName: "calendarlink", // name of the link that is used to toggle + calendarButtonName: "calendarbutton", // name of the button that is used to toggle clockDivName: "clockbox", // name of clock
that gets toggled - clockLinkName: "clocklink", // name of the link that is used to toggle + clockButtonName: "clockbutton", // name of the button that is used to toggle shortCutsClass: "datetimeshortcuts", // class of the clock and cal shortcuts timezoneWarningClass: "timezonewarning", // class of the warning for timezone mismatch timezoneOffset: 0, @@ -128,28 +128,28 @@ const shortcuts_span = document.createElement("span"); shortcuts_span.className = DateTimeShortcuts.shortCutsClass; inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); - const now_link = document.createElement("a"); - now_link.href = "#"; - now_link.textContent = gettext("Now"); - now_link.role = "button"; - now_link.addEventListener("click", function (e) { + const now_button = document.createElement("button"); + now_button.textContent = gettext("Now"); + now_button.type = "button"; + now_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleClockQuicklink(num, -1); }); - const clock_link = document.createElement("button"); - clock_link.type = "button"; - clock_link.id = DateTimeShortcuts.clockLinkName + num; - clock_link.addEventListener("click", function (e) { + const clock_button = document.createElement("button"); + clock_button.type = "button"; + clock_button.id = DateTimeShortcuts.clockButtonName + num; + clock_button.addEventListener("click", function (e) { e.preventDefault(); // avoid triggering the document click handler to dismiss the clock e.stopPropagation(); DateTimeShortcuts.openClock(num); }); - const clockIconId = DateTimeShortcuts.clockLinkName + num + "_icon"; + const clockIconId = + DateTimeShortcuts.clockButtonName + num + "_icon"; quickElement( "span", - clock_link, + clock_button, "", "id", clockIconId, @@ -158,13 +158,13 @@ "title", gettext("Choose a Time"), ); - clock_link.setAttribute("aria-labelledby", clockIconId); + clock_button.setAttribute("aria-labelledby", clockIconId); shortcuts_span.appendChild(document.createTextNode("\u00A0")); - shortcuts_span.appendChild(now_link); + shortcuts_span.appendChild(now_button); shortcuts_span.appendChild( document.createTextNode("\u00A0|\u00A0"), ); - shortcuts_span.appendChild(clock_link); + shortcuts_span.appendChild(clock_button); // Create clock link div // @@ -173,14 +173,14 @@ // aria-label="Choose a time"> //

Choose a time

// //

- // Cancel + // //

// @@ -205,16 +205,14 @@ ? "default_" : inp.name; DateTimeShortcuts.clockHours[name].forEach(function (element) { - const time_link = quickElement( - "a", + const time_button = quickElement( + "button", quickElement("li", time_list), gettext(element[0]), - "role", + "type", "button", - "href", - "#", ); - time_link.addEventListener("click", function (e) { + time_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleClockQuicklink(num, element[1]); }); @@ -222,16 +220,14 @@ const cancel_p = quickElement("p", clock_box); cancel_p.className = "calendar-cancel"; - const cancel_link = quickElement( - "a", + const cancel_button = quickElement( + "button", cancel_p, gettext("Cancel"), - "role", + "type", "button", - "href", - "#", ); - cancel_link.addEventListener("click", function (e) { + cancel_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.dismissClock(num); }); @@ -248,20 +244,21 @@ const clock_box = document.getElementById( DateTimeShortcuts.clockDivName + num, ); - const clock_link = document.getElementById( - DateTimeShortcuts.clockLinkName + num, + const clock_button = document.getElementById( + DateTimeShortcuts.clockButtonName + num, ); // Recalculate the clockbox position // is it left-to-right or right-to-left layout ? if (window.getComputedStyle(document.body).direction !== "rtl") { - clock_box.style.left = findPosX(clock_link) + 17 + "px"; + clock_box.style.left = findPosX(clock_button) + 17 + "px"; } else { // since style's width is in em, it'd be tough to calculate // px value of it. let's use an estimated px for now - clock_box.style.right = findPosX(clock_link) - 110 + "px"; + clock_box.style.right = findPosX(clock_button) - 110 + "px"; } - clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + "px"; + clock_box.style.top = + Math.max(0, findPosY(clock_button) - 30) + "px"; // Show the clock box clock_box.showModal(); @@ -307,11 +304,10 @@ const shortcuts_span = document.createElement("span"); shortcuts_span.className = DateTimeShortcuts.shortCutsClass; inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); - const today_link = document.createElement("a"); - today_link.href = "#"; - today_link.role = "button"; - today_link.appendChild(document.createTextNode(gettext("Today"))); - today_link.setAttribute( + const today_button = document.createElement("button"); + today_button.type = "button"; + today_button.appendChild(document.createTextNode(gettext("Today"))); + today_button.setAttribute( "aria-label", interpolate( gettext("Today (%(date)s)"), @@ -319,24 +315,24 @@ true, ), ); - today_link.addEventListener("click", function (e) { + today_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, 0); }); - const cal_link = document.createElement("button"); - cal_link.type = "button"; - cal_link.id = DateTimeShortcuts.calendarLinkName + num; - cal_link.addEventListener("click", function (e) { + const cal_button = document.createElement("button"); + cal_button.type = "button"; + cal_button.id = DateTimeShortcuts.calendarButtonName + num; + cal_button.addEventListener("click", function (e) { e.preventDefault(); // avoid triggering the document click handler to dismiss the calendar e.stopPropagation(); DateTimeShortcuts.openCalendar(num); }); const calIconId = - DateTimeShortcuts.calendarLinkName + num + "_icon"; + DateTimeShortcuts.calendarButtonName + num + "_icon"; quickElement( "span", - cal_link, + cal_button, "", "id", calIconId, @@ -345,13 +341,13 @@ "title", gettext("Choose a Date"), ); - cal_link.setAttribute("aria-labelledby", calIconId); + cal_button.setAttribute("aria-labelledby", calIconId); shortcuts_span.appendChild(document.createTextNode("\u00A0")); - shortcuts_span.appendChild(today_link); + shortcuts_span.appendChild(today_button); shortcuts_span.appendChild( document.createTextNode("\u00A0|\u00A0"), ); - shortcuts_span.appendChild(cal_link); + shortcuts_span.appendChild(cal_button); // Create calendarbox div. // @@ -360,26 +356,26 @@ // //
- // - // + // + // //
//
// //
//
- // Yesterday + // // | - // Today + // // | - // Tomorrow + // //
//

- // Cancel + // //

//
const cal_box = document.createElement("dialog"); @@ -433,14 +429,26 @@ }); // next-prev links const cal_nav = quickElement("div", cal_box); - const cal_nav_prev = quickElement("a", cal_nav, "<", "href", "#"); + const cal_nav_prev = quickElement( + "button", + cal_nav, + "<", + "type", + "button", + ); cal_nav_prev.className = "calendarnav-previous"; cal_nav_prev.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.drawPrev(num); }); - const cal_nav_next = quickElement("a", cal_nav, ">", "href", "#"); + const cal_nav_next = quickElement( + "button", + cal_nav, + ">", + "type", + "button", + ); cal_nav_next.className = "calendarnav-next"; cal_nav_next.addEventListener("click", function (e) { e.preventDefault(); @@ -465,16 +473,14 @@ // calendar shortcuts const shortcuts = quickElement("div", cal_box); shortcuts.className = "calendar-shortcuts"; - let day_link = quickElement( - "a", + let day_button = quickElement( + "button", shortcuts, gettext("Yesterday"), - "role", + "type", "button", - "href", - "#", ); - day_link.setAttribute( + day_button.setAttribute( "aria-label", interpolate( gettext("Yesterday (%(date)s)"), @@ -482,21 +488,19 @@ true, ), ); - day_link.addEventListener("click", function (e) { + day_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, -1); }); shortcuts.appendChild(document.createTextNode("\u00A0|\u00A0")); - day_link = quickElement( - "a", + day_button = quickElement( + "button", shortcuts, gettext("Today"), - "role", + "type", "button", - "href", - "#", ); - day_link.setAttribute( + day_button.setAttribute( "aria-label", interpolate( gettext("Today (%(date)s)"), @@ -504,21 +508,19 @@ true, ), ); - day_link.addEventListener("click", function (e) { + day_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, 0); }); shortcuts.appendChild(document.createTextNode("\u00A0|\u00A0")); - day_link = quickElement( - "a", + day_button = quickElement( + "button", shortcuts, gettext("Tomorrow"), - "role", + "type", "button", - "href", - "#", ); - day_link.setAttribute( + day_button.setAttribute( "aria-label", interpolate( gettext("Tomorrow (%(date)s)"), @@ -526,7 +528,7 @@ true, ), ); - day_link.addEventListener("click", function (e) { + day_button.addEventListener("click", function (e) { e.preventDefault(); DateTimeShortcuts.handleCalendarQuickLink(num, +1); }); @@ -535,13 +537,11 @@ const cancel_p = quickElement("p", cal_box); cancel_p.className = "calendar-cancel"; const cancel_link = quickElement( - "a", + "button", cancel_p, gettext("Cancel"), - "role", + "type", "button", - "href", - "#", ); cancel_link.addEventListener("click", function (e) { e.preventDefault(); @@ -594,8 +594,8 @@ const cal_box = document.getElementById( DateTimeShortcuts.calendarDivName1 + num, ); - const cal_link = document.getElementById( - DateTimeShortcuts.calendarLinkName + num, + const cal_button = document.getElementById( + DateTimeShortcuts.calendarButtonName + num, ); const inp = DateTimeShortcuts.calendarInputs[num]; @@ -619,13 +619,13 @@ // Recalculate the calendarbox position // is it left-to-right or right-to-left layout ? if (window.getComputedStyle(document.body).direction !== "rtl") { - cal_box.style.left = findPosX(cal_link) + 17 + "px"; + cal_box.style.left = findPosX(cal_button) + 17 + "px"; } else { // since style's width is in em, it'd be tough to calculate // px value of it. let's use an estimated px for now - cal_box.style.right = findPosX(cal_link) - 180 + "px"; + cal_box.style.right = findPosX(cal_button) - 180 + "px"; } - cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + "px"; + cal_box.style.top = Math.max(0, findPosY(cal_button) - 75) + "px"; cal_box.showModal(); DateTimeShortcuts.updateNavAriaLabels(num); diff --git a/django/contrib/admin/static/admin/js/calendar.js b/django/contrib/admin/static/admin/js/calendar.js index b9b45b26186b..457062b48432 100644 --- a/django/contrib/admin/static/admin/js/calendar.js +++ b/django/contrib/admin/static/admin/js/calendar.js @@ -223,14 +223,12 @@ depends on core.js for utility functions like removeChildren or quickElement "class", todayClass, ); - const link = quickElement( - "a", + const button = quickElement( + "button", cell, currentDay, - "role", + "type", "button", - "href", - "#", ); let ariaLabel = CalendarNamespace.formatDate( currentDay, @@ -255,8 +253,8 @@ depends on core.js for utility functions like removeChildren or quickElement ariaLabel, ]); } - link.setAttribute("aria-label", ariaLabel); - link.addEventListener("click", calendarMonth(year, month)); + button.setAttribute("aria-label", ariaLabel); + button.addEventListener("click", calendarMonth(year, month)); currentDay++; } diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index 994425229503..174544cced0a 100644 --- a/django/contrib/admin/static/admin/js/inlines.js +++ b/django/contrib/admin/static/admin/js/inlines.js @@ -67,11 +67,11 @@ options.addCssClass + '">' + + '">", ); - addButton = $parent.find("tr:last a"); + addButton = $parent.find("tr:last button"); } else { // Otherwise, insert it immediately after the last form: $this @@ -79,11 +79,11 @@ .after( '", + "
", ); - addButton = $this.filter(":last").next().find("a"); + addButton = $this.filter(":last").next().find("button"); } } addButton.on("click", addInlineClickHandler); @@ -138,35 +138,35 @@ // If the forms are laid out in table rows, insert // the remove button into the last table cell: row.children(":last").append( - '
' + + '">' + options.deleteText + - "
", + "
", ); } else if (row.is("ul") || row.is("ol")) { // If they're laid out as an ordered/unordered list, // insert an
  • after the last list item: row.append( - '
  • ' + + '">' + options.deleteText + - "
  • ", + "", ); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: row.children(":first").append( - '' + + '">' + options.deleteText + - "", + "", ); } // Add delete handler for each row. - row.find("a." + options.deleteCssClass).on( + row.find("button." + options.deleteCssClass).on( "click", inlineDeleteHandler.bind(this), ); diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html index ff2a5fe3d7ba..1987b5b51e4e 100644 --- a/django/contrib/admin/templates/admin/actions.html +++ b/django/contrib/admin/templates/admin/actions.html @@ -13,9 +13,9 @@ {% if cl.result_count != cl.result_list|length %} - + {% endif %} {% endif %} {% endblock %} diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html index bea4a5b859a5..d2a9feed2085 100644 --- a/django/contrib/admin/templates/admin/change_list_results.html +++ b/django/contrib/admin/templates/admin/change_list_results.html @@ -18,7 +18,7 @@
    {% endif %} -
    {% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
    +
    {% if header.sortable %}{{ header.text|capfirst }}{% else %}{{ header.text|capfirst }}{% endif %}
    {% endfor %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 7797f44eb394..c4d33bdb531c 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -44,7 +44,7 @@

    {% translate "Objects" %}

    {% if is_popup %}{% endif %} {% if to_field %}{% endif %} - {% translate "No, take me back" %} + {% translate "No, take me back" %} {% endblock %} diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html index cf503ec1230b..a9245d620df3 100644 --- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -40,7 +40,7 @@

    {% translate "Objects" %}

    - {% translate "No, take me back" %} + {% translate "No, take me back" %} {% endif %} diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html index 5390dd36c167..b2b205496612 100644 --- a/django/contrib/admin/templates/admin/submit_line.html +++ b/django/contrib/admin/templates/admin/submit_line.html @@ -7,11 +7,11 @@ {% if show_save_and_continue %}{% endif %} {% if show_close %} {% url opts|admin_urlname:'changelist' as changelist_url %} - {% translate 'Close' %} + {% translate 'Close' %} {% endif %} {% if show_delete_link and original %} {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} - {% translate "Delete" %} + {% translate "Delete" %} {% endif %} {% endblock %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index a6adafabbefb..aa3e76fdd2fc 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -43,12 +43,12 @@ def paginator_number(cl, i): return format_html("{} ", cl.paginator.ELLIPSIS) elif i == cl.page_num: return format_html( - '{} ', + '{} ', i, ) else: return format_html( - '{} ', + '{} ', cl.get_query_string({PAGE_VAR: i}), i, ) diff --git a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html index 102ceb30eb7d..a9b56a3331a2 100644 --- a/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html +++ b/django/contrib/auth/templates/auth/widgets/read_only_password_hash.html @@ -1,5 +1,5 @@ {% load auth %} {% render_password_as_hash widget.value %} -

    {{ button_label }}

    +

    {{ button_label }}

    diff --git a/js_tests/admin/DateTimeShortcuts.test.js b/js_tests/admin/DateTimeShortcuts.test.js index 865aed2e6863..b03e2dfca543 100644 --- a/js_tests/admin/DateTimeShortcuts.test.js +++ b/js_tests/admin/DateTimeShortcuts.test.js @@ -23,7 +23,7 @@ QUnit.test("init", function (assert) { const shortcuts = $(".datetimeshortcuts"); assert.equal(shortcuts.length, 1); - assert.equal(shortcuts.find("a:first").text(), "Today"); + assert.equal(shortcuts.find("button:first").text(), "Today"); assert.equal(shortcuts.find("button:last .date-icon").length, 1); // To prevent incorrect timezone warnings on date/time widgets, timezoneOffset @@ -39,7 +39,7 @@ QUnit.test("custom time shortcuts", function (assert) { $("#qunit-fixture").append(timeField); DateTimeShortcuts.clockHours.time_test = [["3 a.m.", 3]]; DateTimeShortcuts.init(); - assert.equal($(".clockbox").find("a").first().text(), "3 a.m."); + assert.equal($(".clockbox").find("button").first().text(), "3 a.m."); }); QUnit.test("time zone offset warning - single field", function (assert) { @@ -125,7 +125,7 @@ QUnit.test("today link has aria-label with current date", function (assert) { ); $("#qunit-fixture").append(dateField); DateTimeShortcuts.init(); - const todayLink = $(".datetimeshortcuts a:first"); + const todayLink = $(".datetimeshortcuts button:first"); assert.equal(todayLink.text(), "Today"); // "Today (April 12, 2026)" const today = new Date(); @@ -163,7 +163,7 @@ QUnit.test("calendar today highlight with server offset", function (assert) { const todayCells = calDiv.find("td.today"); assert.equal(todayCells.length, 1, "Exactly one cell marked as today"); assert.equal( - todayCells.find("a").text(), + todayCells.find("button").text(), String(expectedDate.getDate()), "Today cell matches server-adjusted date", ); diff --git a/js_tests/admin/inlines.test.js b/js_tests/admin/inlines.test.js index 28de67cf65c2..62b87842ca60 100644 --- a/js_tests/admin/inlines.test.js +++ b/js_tests/admin/inlines.test.js @@ -20,18 +20,18 @@ QUnit.module("admin.inlines: tabular formsets", { QUnit.test("no forms", function (assert) { assert.ok(this.inlineRow.hasClass("dynamic-first")); - assert.equal(this.table.find(".add-row a").text(), this.addText); + assert.equal(this.table.find(".add-row button").text(), this.addText); }); QUnit.test("add form", function (assert) { - const addButton = this.table.find(".add-row a"); + const addButton = this.table.find(".add-row button"); assert.equal(addButton.text(), this.addText); addButton.click(); assert.ok(this.table.find("#first-1")); }); QUnit.test("added form has remove button", function (assert) { - const addButton = this.table.find(".add-row a"); + const addButton = this.table.find(".add-row button"); assert.equal(addButton.text(), this.addText); addButton.click(); assert.equal(this.table.find("#first-1 .inline-deletelink").length, 1); @@ -39,7 +39,7 @@ QUnit.test("added form has remove button", function (assert) { QUnit.test("add/remove form events", function (assert) { assert.expect(5); - const addButton = this.table.find(".add-row a"); + const addButton = this.table.find(".add-row button"); document.addEventListener( "formset:added", (event) => { @@ -75,7 +75,7 @@ QUnit.test("existing add button", function (assert) { deleteText: "Remove", addButton: addButton, }); - assert.equal(this.table.find(".add-row a").length, 0); + assert.equal(this.table.find(".add-row button").length, 0); addButton.click(); assert.ok(this.table.find("#first-1")); }); @@ -126,7 +126,7 @@ QUnit.test( const tr = this.inlineRows.slice(1, 2); const trWithErrors = tr.prev(); assert.ok(trWithErrors.hasClass("row-form-errors")); - const deleteLink = tr.find("a.inline-deletelink"); + const deleteLink = tr.find("button.inline-deletelink"); deleteLink.trigger($.Event("click", { target: deleteLink })); assert.notOk(this.table.find(".row-form-errors").length); }, @@ -151,17 +151,17 @@ QUnit.module("admin.inlines: tabular formsets with max_num", { QUnit.test( "does not show the add button if already at max_num", function (assert) { - const addButton = this.table.find("tr.add_row > td > a"); + const addButton = this.table.find("tr.add_row > td > button"); assert.notOk(addButton.is(":visible")); }, ); QUnit.test("make addButton visible again", function (assert) { const $ = django.jQuery; - const addButton = this.table.find("tr.add_row > td > a"); + const addButton = this.table.find("tr.add_row > td > button"); const removeButton = this.table .find("tr.form-row:first") - .find("a.inline-deletelink"); + .find("button.inline-deletelink"); removeButton.trigger($.Event("click", { target: removeButton })); assert.notOk(addButton.is(":visible")); }); @@ -191,7 +191,7 @@ QUnit.test( QUnit.test("make removeButtons visible again", function (assert) { const $ = django.jQuery; - const addButton = this.table.find("tr.add-row > td > a"); + const addButton = this.table.find("tr.add-row > td > button"); addButton.trigger($.Event("click", { target: addButton })); assert.equal(this.table.find(".inline-deletelink:visible").length, 2); }); diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index ba1f5d179365..f66d847dfe36 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -1053,11 +1053,11 @@ def test_pagination_render(self): ) self.assertTrue(pagination_output.endswith("")) self.assertInHTML( - '
  • 1
  • ', + '
  • 1
  • ', pagination_output, ) self.assertInHTML( - '
  • 2
  • ', + '
  • 2
  • ', pagination_output, ) self.assertEqual(pagination_output.count('aria-current="page"'), 1) diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index eda7c91310e9..5dcbbd669b47 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -1462,12 +1462,11 @@ def test_submit_line_shows_only_close_button(self): response = self.client.get(self.change_url) self.assertContains( response, - '' - "Close", + '' "Close", html=True, ) delete_link = ( - 'Delete' ) self.assertNotContains(response, delete_link % self.poll.id, html=True) @@ -1908,7 +1907,7 @@ def test_add_stackeds(self): self.assertCountSeleniumElements(rows_selector, 3) add_button = self.selenium.find_element( - By.LINK_TEXT, "Add another Inner4 stacked" + By.XPATH, "//button[contains(text(), 'Add another Inner4 stacked')]" ) add_button.click() self.assertCountSeleniumElements(rows_selector, 4) @@ -1928,7 +1927,7 @@ def test_delete_stackeds(self): self.assertCountSeleniumElements(rows_selector, 3) add_button = self.selenium.find_element( - By.LINK_TEXT, "Add another Inner4 stacked" + By.XPATH, "//button[contains(text(), 'Add another Inner4 stacked')]" ) add_button.click() add_button.click() @@ -1956,8 +1955,8 @@ def test_delete_invalid_stacked_inlines(self): self.assertCountSeleniumElements(rows_selector, 3) add_button = self.selenium.find_element( - By.LINK_TEXT, - "Add another Inner4 stacked", + By.XPATH, + "//button[contains(text(), 'Add another Inner4 stacked')]", ) add_button.click() add_button.click() @@ -2022,7 +2021,7 @@ def test_delete_invalid_tabular_inlines(self): self.assertCountSeleniumElements(rows_selector, 3) add_button = self.selenium.find_element( - By.LINK_TEXT, "Add another Inner4 tabular" + By.XPATH, "//button[contains(text(), 'Add another Inner4 tabular')]" ) add_button.click() add_button.click() @@ -2103,7 +2102,9 @@ def test_add_inlines(self): ) # Add an inline - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() # The inline has been added, it has the right id, and it contains the # correct fields. @@ -2121,7 +2122,9 @@ def test_add_inlines(self): ".dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]", 1 ) # Let's add another one to be sure - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() self.assertCountSeleniumElements(".dynamic-profile_set", 3) self.assertEqual( self.selenium.find_elements(By.CSS_SELECTOR, ".dynamic-profile_set")[ @@ -2186,7 +2189,9 @@ def test_add_inline_link_absent_for_view_only_parent_model(self): self.selenium.get(self.live_server_url + change_url) with self.disable_implicit_wait(): with self.assertRaises(NoSuchElementException): - self.selenium.find_element(By.LINK_TEXT, "Add another Question") + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Question')]" + ) def test_delete_inlines(self): from selenium.webdriver.common.by import By @@ -2197,10 +2202,18 @@ def test_delete_inlines(self): ) # Add a few inlines - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() - self.selenium.find_element(By.LINK_TEXT, "Add another Profile").click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Profile')]" + ).click() self.assertCountSeleniumElements( "#profile_set-group table tr.dynamic-profile_set", 5 ) @@ -2223,12 +2236,12 @@ def test_delete_inlines(self): self.selenium.find_element( By.CSS_SELECTOR, "form#profilecollection_form tr.dynamic-profile_set#profile_set-1 " - "td.delete a", + "td.delete button", ).click() self.selenium.find_element( By.CSS_SELECTOR, "form#profilecollection_form tr.dynamic-profile_set#profile_set-2 " - "td.delete a", + "td.delete button", ).click() # The rows are gone and the IDs have been re-sequenced self.assertCountSeleniumElements( @@ -2281,7 +2294,9 @@ def test_added_stacked_inline_with_collapsed_fields(self): self.live_server_url + reverse("admin:admin_inlines_teacher_add") ) add_text = gettext("Add another %(verbose_name)s") % {"verbose_name": "Child"} - self.selenium.find_element(By.LINK_TEXT, add_text).click() + self.selenium.find_element( + By.XPATH, f"//button[contains(text(), '{add_text}')]" + ).click() test_fields = ["#id_child_set-0-name", "#id_child_set-1-name"] summaries = self.selenium.find_elements(By.TAG_NAME, "summary") self.assertEqual(len(summaries), 3) @@ -2448,7 +2463,9 @@ def test_inlines_verbose_name(self): self.assertIn("Available attendant", available.text) self.assertIn("Chosen attendant", chosen.text) # Added inline should also have the correct verbose_name. - self.selenium.find_element(By.LINK_TEXT, "Add another Class").click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Class')]" + ).click() available = self.selenium.find_element( By.CSS_SELECTOR, css_available_selector % 1 ) @@ -2458,7 +2475,9 @@ def test_inlines_verbose_name(self): self.assertIn("Available attendant", available.text) self.assertIn("Chosen attendant", chosen.text) # Third inline should also have the correct verbose_name. - self.selenium.find_element(By.LINK_TEXT, "Add another Class").click() + self.selenium.find_element( + By.XPATH, "//button[contains(text(), 'Add another Class')]" + ).click() available = self.selenium.find_element( By.CSS_SELECTOR, css_available_selector % 2 ) @@ -2540,7 +2559,7 @@ def test_tabular_inline_delete_layout(self): "fieldset.module tbody tr.dynamic-sighting_set:not(.original) td.delete", ) self.assertIn( - '', + '