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
3 changes: 2 additions & 1 deletion django/contrib/admin/static/admin/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ a:not(
[role="button"],
#header a,
#nav-sidebar a,
#content-main.app-list a
#content-main.app-list a,
.object-tools a,
) {
text-decoration: underline;
}
Expand Down
16 changes: 12 additions & 4 deletions django/contrib/admin/static/admin/css/changelists.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/* CHANGELISTS */

#changelist {
#changelist .changelist-form-container {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
width: 100%;
}

#changelist .changelist-form-container {
#changelist .changelist-form-container > div {
flex: 1 1 auto;
min-width: 0;
}

#changelist .changelist-form-container:not(:has(#changelist-filter)) > div {
width: 100%;
}

#changelist .changelist-form-container:has(#changelist-filter) > div {
max-width: calc(100% - 270px);
}

#changelist table {
Expand Down
11 changes: 7 additions & 4 deletions django/contrib/admin/static/admin/css/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,15 @@ input[type="submit"], button {

/* Changelist */

#changelist {
align-items: stretch;
#changelist .changelist-form-container {
flex-direction: column;
}

#changelist .changelist-form-container:has(#changelist-filter) > div {
max-width: 100%;
width: 100%;
}

#toolbar {
padding: 10px;
}
Expand All @@ -436,8 +440,7 @@ input[type="submit"], button {
}

#changelist-filter {
position: static;
width: auto;
width: 100%;
margin-top: 30px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% block object-tools-items %}
<li>
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a role="button" href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
</li>
{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a></li>{% endif %}
{% endblock %}
52 changes: 27 additions & 25 deletions django/contrib/admin/templates/admin/change_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,8 @@
{% endif %}
<div class="module{% if cl.has_filters %} filtered{% endif %}" id="changelist">
<div class="changelist-form-container">
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}

<form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
{% if cl.formset %}
<div>{{ cl.formset.management_form }}</div>
{% endif %}

{% block result_list %}
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% result_list cl %}
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% endblock %}
{% block pagination %}
<div class="changelist-footer">
{% pagination cl %}
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
{% endblock %}
</div>
</form>
</div>
{% block filters %}
{% if cl.has_filters %}
{% block filters %}
{% if cl.has_filters %}
<search id="changelist-filter" aria-labelledby="changelist-filter-header">
<h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
{% if cl.is_facets_optional or cl.has_active_filters %}<div id="changelist-filter-extra-actions">
Expand All @@ -92,8 +71,31 @@ <h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
</div>{% endif %}
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
</search>
{% endif %}
{% endblock %}
{% endif %}
{% endblock %}
<div>
{% block search %}{% search_form cl %}{% endblock %}
{% block date_hierarchy %}{% if cl.date_hierarchy %}{% date_hierarchy cl %}{% endif %}{% endblock %}

<form id="changelist-form" method="post"{% if cl.formset and cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %} novalidate>{% csrf_token %}
{% if cl.formset %}
<div>{{ cl.formset.management_form }}</div>
{% endif %}

{% block result_list %}
{% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% result_list cl %}
{% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %}
{% endblock %}
{% block pagination %}
<div class="changelist-footer">
{% pagination cl %}
{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% translate 'Save' %}">{% endif %}
{% endblock %}
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% if has_add_permission %}
<li>
{% url cl.opts|admin_urlname:'add' as add_url %}
<a role="button" href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
{% blocktranslate with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktranslate %}
</a>
</li>
Expand Down
2 changes: 2 additions & 0 deletions tests/admin_views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ class PodcastAdmin(admin.ModelAdmin):
list_display = ("name", "release_date")
list_editable = ("release_date",)
date_hierarchy = "release_date"
list_filter = ("name",)
search_fields = ("name",)
ordering = ("name",)


Expand Down
44 changes: 44 additions & 0 deletions tests/admin_views/test_skip_link_to_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.test import override_settings
from django.urls import reverse

from .models import Podcast


@override_settings(ROOT_URLCONF="admin_views.urls")
class SeleniumTests(AdminSeleniumTestCase):
Expand Down Expand Up @@ -125,3 +127,45 @@ def test_skip_link_with_RTL_language_doesnt_create_horizontal_scrolling(self):
)
self.assertTrue(is_vertical_scrolleable)
self.assertFalse(is_horizontal_scrolleable)

def test_skip_link_keyboard_navigation_in_changelist(self):
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

Podcast.objects.create(name="apple", release_date="2000-09-19")
self.admin_login(
username="super", password="secret", login_url=reverse("admin:index")
)
self.selenium.get(
self.live_server_url + reverse("admin:admin_views_podcast_changelist")
)
selectors = [
"ul.object-tools", # object_tools.
"search#changelist-filter", # list_filter.
"form#changelist-search", # search_fields.
"nav.toplinks", # date_hierarchy.
"form#changelist-form div.actions", # action.
"table#result_list", # table.
"div.changelist-footer", # footer.
]
content = self.selenium.find_element(By.ID, "content-start")
content.send_keys(Keys.TAB)

for selector in selectors:
with self.subTest(selector=selector):
# Currently focused element.
focused_element = self.selenium.switch_to.active_element
expected_element = self.selenium.find_element(By.CSS_SELECTOR, selector)
element_points = self.selenium.find_elements(
By.CSS_SELECTOR,
f"{selector} a, {selector} input, {selector} button",
)
self.assertIn(
focused_element.get_attribute("outerHTML"),
expected_element.get_attribute("innerHTML"),
)
# Move to the next container element via TAB.
for point in element_points[::-1]:
if point.is_displayed():
point.send_keys(Keys.TAB)
break
32 changes: 30 additions & 2 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4086,7 +4086,7 @@ def test_change_view_history_link(self):
)
self.assertContains(
response,
'<a role="button" href="%s" class="historylink"' % escape(expected_link),
'<a href="%s" class="historylink"' % escape(expected_link),
)

def test_redirect_on_add_view_continue_button(self):
Expand Down Expand Up @@ -6956,6 +6956,34 @@ def test_list_editable_with_filter(self):
with self.wait_page_loaded():
save_button.click()

@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
def test_object_tools(self):
from selenium.webdriver.common.by import By

state = State.objects.create(name="Korea")
city = City.objects.create(state=state, name="Gwangju")
self.admin_login(
username="super", password="secret", login_url=reverse("admin:index")
)
self.selenium.get(
self.live_server_url + reverse("admin:admin_views_city_changelist")
)
object_tools = self.selenium.find_elements(
By.CSS_SELECTOR, "ul.object-tools li a"
)
self.assertEqual(len(object_tools), 1)
self.take_screenshot("changelist")

self.selenium.get(
self.live_server_url
+ reverse("admin:admin_views_city_change", args=(city.pk,))
)
object_tools = self.selenium.find_elements(
By.CSS_SELECTOR, "ul.object-tools li a"
)
self.assertEqual(len(object_tools), 2)
self.take_screenshot("changeform")

@screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
def test_long_header_with_object_tools_layout(self):
from selenium.webdriver.common.by import By
Expand Down Expand Up @@ -8415,7 +8443,7 @@ def test_change_view(self):

# Check the history link.
history_link = re.search(
'<a role="button" href="(.*?)" class="historylink">History</a>',
'<a href="(.*?)" class="historylink">History</a>',
response.text,
)
self.assertURLEqual(history_link[1], self.get_history_url())
Expand Down