12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075 |
- import datetime
- from unittest import mock
- from django.contrib import admin
- from django.contrib.admin.models import LogEntry
- from django.contrib.admin.options import IncorrectLookupParameters
- from django.contrib.admin.templatetags.admin_list import pagination
- from django.contrib.admin.tests import AdminSeleniumTestCase
- from django.contrib.admin.views.main import (
- ALL_VAR,
- IS_FACETS_VAR,
- IS_POPUP_VAR,
- ORDER_VAR,
- PAGE_VAR,
- SEARCH_VAR,
- TO_FIELD_VAR,
- )
- from django.contrib.auth.models import User
- from django.contrib.messages.storage.cookie import CookieStorage
- from django.db import DatabaseError, connection, models
- from django.db.models import F, Field, IntegerField
- from django.db.models.functions import Upper
- from django.db.models.lookups import Contains, Exact
- from django.template import Context, Template, TemplateSyntaxError
- from django.test import TestCase, override_settings, skipUnlessDBFeature
- from django.test.client import RequestFactory
- from django.test.utils import CaptureQueriesContext, isolate_apps, register_lookup
- from django.urls import reverse
- from django.utils import formats
- from .admin import (
- BandAdmin,
- ChildAdmin,
- ChordsBandAdmin,
- ConcertAdmin,
- CustomPaginationAdmin,
- CustomPaginator,
- DynamicListDisplayChildAdmin,
- DynamicListDisplayLinksChildAdmin,
- DynamicListFilterChildAdmin,
- DynamicSearchFieldsChildAdmin,
- EmptyValueChildAdmin,
- EventAdmin,
- FilteredChildAdmin,
- GrandChildAdmin,
- GroupAdmin,
- InvitationAdmin,
- NoListDisplayLinksParentAdmin,
- ParentAdmin,
- ParentAdminTwoSearchFields,
- QuartetAdmin,
- SwallowAdmin,
- )
- from .admin import site as custom_site
- from .models import (
- Band,
- CharPK,
- Child,
- ChordsBand,
- ChordsMusician,
- Concert,
- CustomIdUser,
- Event,
- Genre,
- GrandChild,
- Group,
- Invitation,
- Membership,
- Musician,
- OrderedObject,
- Parent,
- Quartet,
- Swallow,
- SwallowOneToOne,
- UnorderedObject,
- )
- def build_tbody_html(obj, href, field_name, extra_fields):
- return (
- "<tbody><tr>"
- '<td class="action-checkbox">'
- '<input type="checkbox" name="_selected_action" value="{}" '
- 'class="action-select" aria-label="Select this object for an action - {}"></td>'
- '<th class="field-name"><a href="{}">{}</a></th>'
- "{}</tr></tbody>"
- ).format(obj.pk, str(obj), href, field_name, extra_fields)
- @override_settings(ROOT_URLCONF="admin_changelist.urls")
- class ChangeListTests(TestCase):
- factory = RequestFactory()
- @classmethod
- def setUpTestData(cls):
- cls.superuser = User.objects.create_superuser(
- username="super", email="a@b.com", password="xxx"
- )
- def _create_superuser(self, username):
- return User.objects.create_superuser(
- username=username, email="a@b.com", password="xxx"
- )
- def _mocked_authenticated_request(self, url, user):
- request = self.factory.get(url)
- request.user = user
- return request
- def test_repr(self):
- m = ChildAdmin(Child, custom_site)
- request = self.factory.get("/child/")
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(repr(cl), "<ChangeList: model=Child model_admin=ChildAdmin>")
- def test_specified_ordering_by_f_expression(self):
- class OrderedByFBandAdmin(admin.ModelAdmin):
- list_display = ["name", "genres", "nr_of_members"]
- ordering = (
- F("nr_of_members").desc(nulls_last=True),
- Upper(F("name")).asc(),
- F("genres").asc(),
- )
- m = OrderedByFBandAdmin(Band, custom_site)
- request = self.factory.get("/band/")
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.get_ordering_field_columns(), {3: "desc", 2: "asc"})
- def test_specified_ordering_by_f_expression_without_asc_desc(self):
- class OrderedByFBandAdmin(admin.ModelAdmin):
- list_display = ["name", "genres", "nr_of_members"]
- ordering = (F("nr_of_members"), Upper("name"), F("genres"))
- m = OrderedByFBandAdmin(Band, custom_site)
- request = self.factory.get("/band/")
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.get_ordering_field_columns(), {3: "asc", 2: "asc"})
- def test_select_related_preserved(self):
- """
- Regression test for #10348: ChangeList.get_queryset() shouldn't
- overwrite a custom select_related provided by ModelAdmin.get_queryset().
- """
- m = ChildAdmin(Child, custom_site)
- request = self.factory.get("/child/")
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.query.select_related, {"parent": {}})
- def test_select_related_preserved_when_multi_valued_in_search_fields(self):
- parent = Parent.objects.create(name="Mary")
- Child.objects.create(parent=parent, name="Danielle")
- Child.objects.create(parent=parent, name="Daniel")
- m = ParentAdmin(Parent, custom_site)
- request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 1)
- # select_related is preserved.
- self.assertEqual(cl.queryset.query.select_related, {"child": {}})
- def test_select_related_as_tuple(self):
- ia = InvitationAdmin(Invitation, custom_site)
- request = self.factory.get("/invitation/")
- request.user = self.superuser
- cl = ia.get_changelist_instance(request)
- self.assertEqual(cl.queryset.query.select_related, {"player": {}})
- def test_select_related_as_empty_tuple(self):
- ia = InvitationAdmin(Invitation, custom_site)
- ia.list_select_related = ()
- request = self.factory.get("/invitation/")
- request.user = self.superuser
- cl = ia.get_changelist_instance(request)
- self.assertIs(cl.queryset.query.select_related, False)
- def test_get_select_related_custom_method(self):
- class GetListSelectRelatedAdmin(admin.ModelAdmin):
- list_display = ("band", "player")
- def get_list_select_related(self, request):
- return ("band", "player")
- ia = GetListSelectRelatedAdmin(Invitation, custom_site)
- request = self.factory.get("/invitation/")
- request.user = self.superuser
- cl = ia.get_changelist_instance(request)
- self.assertEqual(cl.queryset.query.select_related, {"player": {}, "band": {}})
- def test_many_search_terms(self):
- parent = Parent.objects.create(name="Mary")
- Child.objects.create(parent=parent, name="Danielle")
- Child.objects.create(parent=parent, name="Daniel")
- m = ParentAdmin(Parent, custom_site)
- request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel " * 80})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- with CaptureQueriesContext(connection) as context:
- object_count = cl.queryset.count()
- self.assertEqual(object_count, 1)
- self.assertEqual(context.captured_queries[0]["sql"].count("JOIN"), 1)
- def test_related_field_multiple_search_terms(self):
- """
- Searches over multi-valued relationships return rows from related
- models only when all searched fields match that row.
- """
- parent = Parent.objects.create(name="Mary")
- Child.objects.create(parent=parent, name="Danielle", age=18)
- Child.objects.create(parent=parent, name="Daniel", age=19)
- m = ParentAdminTwoSearchFields(Parent, custom_site)
- request = self.factory.get("/parent/", data={SEARCH_VAR: "danielle 19"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 0)
- request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel 19"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 1)
- def test_result_list_empty_changelist_value(self):
- """
- Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
- for relationship fields
- """
- new_child = Child.objects.create(name="name", parent=None)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = ChildAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- cl.formset = None
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
- row_html = build_tbody_html(
- new_child, link, "name", '<td class="field-parent nowrap">-</td>'
- )
- self.assertNotEqual(
- table_output.find(row_html),
- -1,
- "Failed to find expected row element: %s" % table_output,
- )
- def test_result_list_empty_changelist_value_blank_string(self):
- new_child = Child.objects.create(name="", parent=None)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = ChildAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- cl.formset = None
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
- row_html = build_tbody_html(
- new_child, link, "-", '<td class="field-parent nowrap">-</td>'
- )
- self.assertInHTML(row_html, table_output)
- def test_result_list_set_empty_value_display_on_admin_site(self):
- """
- Empty value display can be set on AdminSite.
- """
- new_child = Child.objects.create(name="name", parent=None)
- request = self.factory.get("/child/")
- request.user = self.superuser
- # Set a new empty display value on AdminSite.
- admin.site.empty_value_display = "???"
- m = ChildAdmin(Child, admin.site)
- cl = m.get_changelist_instance(request)
- cl.formset = None
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
- row_html = build_tbody_html(
- new_child, link, "name", '<td class="field-parent nowrap">???</td>'
- )
- self.assertNotEqual(
- table_output.find(row_html),
- -1,
- "Failed to find expected row element: %s" % table_output,
- )
- def test_result_list_set_empty_value_display_in_model_admin(self):
- """
- Empty value display can be set in ModelAdmin or individual fields.
- """
- new_child = Child.objects.create(name="name", parent=None)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = EmptyValueChildAdmin(Child, admin.site)
- cl = m.get_changelist_instance(request)
- cl.formset = None
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
- row_html = build_tbody_html(
- new_child,
- link,
- "name",
- '<td class="field-age_display">&dagger;</td>'
- '<td class="field-age">-empty-</td>',
- )
- self.assertNotEqual(
- table_output.find(row_html),
- -1,
- "Failed to find expected row element: %s" % table_output,
- )
- def test_result_list_html(self):
- """
- Inclusion tag result_list generates a table when with default
- ModelAdmin settings.
- """
- new_parent = Parent.objects.create(name="parent")
- new_child = Child.objects.create(name="name", parent=new_parent)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = ChildAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- cl.formset = None
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- link = reverse("admin:admin_changelist_child_change", args=(new_child.id,))
- row_html = build_tbody_html(
- new_child,
- link,
- "name",
- '<td class="field-parent nowrap">%s</td>' % new_parent,
- )
- self.assertNotEqual(
- table_output.find(row_html),
- -1,
- "Failed to find expected row element: %s" % table_output,
- )
- self.assertInHTML(
- '<input type="checkbox" id="action-toggle" '
- 'aria-label="Select all objects on this page for an action">',
- table_output,
- )
- def test_result_list_editable_html(self):
- """
- Regression tests for #11791: Inclusion tag result_list generates a
- table and this checks that the items are nested within the table
- element tags.
- Also a regression test for #13599, verifies that hidden fields
- when list_editable is enabled are rendered in a div outside the
- table.
- """
- new_parent = Parent.objects.create(name="parent")
- new_child = Child.objects.create(name="name", parent=new_parent)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = ChildAdmin(Child, custom_site)
- # Test with list_editable fields
- m.list_display = ["id", "name", "parent"]
- m.list_display_links = ["id"]
- m.list_editable = ["name"]
- cl = m.get_changelist_instance(request)
- FormSet = m.get_changelist_formset(request)
- cl.formset = FormSet(queryset=cl.result_list)
- template = Template(
- "{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}"
- )
- context = Context({"cl": cl, "opts": Child._meta})
- table_output = template.render(context)
- # make sure that hidden fields are in the correct place
- hiddenfields_div = (
- '<div class="hiddenfields">'
- '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
- "</div>"
- ) % new_child.id
- self.assertInHTML(
- hiddenfields_div, table_output, msg_prefix="Failed to find hidden fields"
- )
- # make sure that list editable fields are rendered in divs correctly
- editable_name_field = (
- '<input name="form-0-name" value="name" class="vTextField" '
- 'maxlength="30" type="text" id="id_form-0-name">'
- )
- self.assertInHTML(
- '<td class="field-name">%s</td>' % editable_name_field,
- table_output,
- msg_prefix='Failed to find "name" list_editable field',
- )
- def test_result_list_editable(self):
- """
- Regression test for #14312: list_editable with pagination
- """
- new_parent = Parent.objects.create(name="parent")
- for i in range(1, 201):
- Child.objects.create(name="name %s" % i, parent=new_parent)
- request = self.factory.get("/child/", data={"p": -1}) # Anything outside range
- request.user = self.superuser
- m = ChildAdmin(Child, custom_site)
- # Test with list_editable fields
- m.list_display = ["id", "name", "parent"]
- m.list_display_links = ["id"]
- m.list_editable = ["name"]
- with self.assertRaises(IncorrectLookupParameters):
- m.get_changelist_instance(request)
- @skipUnlessDBFeature("supports_transactions")
- def test_list_editable_atomicity(self):
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- self.client.force_login(self.superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- data = {
- "form-TOTAL_FORMS": "2",
- "form-INITIAL_FORMS": "2",
- "form-MIN_NUM_FORMS": "0",
- "form-MAX_NUM_FORMS": "1000",
- "form-0-uuid": str(a.pk),
- "form-1-uuid": str(b.pk),
- "form-0-load": "9.0",
- "form-0-speed": "3.0",
- "form-1-load": "5.0",
- "form-1-speed": "1.0",
- "_save": "Save",
- }
- with mock.patch(
- "django.contrib.admin.ModelAdmin.log_change", side_effect=DatabaseError
- ):
- with self.assertRaises(DatabaseError):
- self.client.post(changelist_url, data)
- # Original values are preserved.
- a.refresh_from_db()
- self.assertEqual(a.load, 4)
- self.assertEqual(a.speed, 1)
- b.refresh_from_db()
- self.assertEqual(b.load, 2)
- self.assertEqual(b.speed, 2)
- with mock.patch(
- "django.contrib.admin.ModelAdmin.log_change",
- side_effect=[None, DatabaseError],
- ):
- with self.assertRaises(DatabaseError):
- self.client.post(changelist_url, data)
- # Original values are preserved.
- a.refresh_from_db()
- self.assertEqual(a.load, 4)
- self.assertEqual(a.speed, 1)
- b.refresh_from_db()
- self.assertEqual(b.load, 2)
- self.assertEqual(b.speed, 2)
- def test_custom_paginator(self):
- new_parent = Parent.objects.create(name="parent")
- for i in range(1, 201):
- Child.objects.create(name="name %s" % i, parent=new_parent)
- request = self.factory.get("/child/")
- request.user = self.superuser
- m = CustomPaginationAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- self.assertIsInstance(cl.paginator, CustomPaginator)
- def test_distinct_for_m2m_in_list_filter(self):
- """
- Regression test for #13902: When using a ManyToMany in list_filter,
- results shouldn't appear more than once. Basic ManyToMany.
- """
- blues = Genre.objects.create(name="Blues")
- band = Band.objects.create(name="B.B. King Review", nr_of_members=11)
- band.genres.add(blues)
- band.genres.add(blues)
- m = BandAdmin(Band, custom_site)
- request = self.factory.get("/band/", data={"genres": blues.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- # There's only one Group instance
- self.assertEqual(cl.result_count, 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_distinct_for_through_m2m_in_list_filter(self):
- """
- Regression test for #13902: When using a ManyToMany in list_filter,
- results shouldn't appear more than once. With an intermediate model.
- """
- lead = Musician.objects.create(name="Vox")
- band = Group.objects.create(name="The Hype")
- Membership.objects.create(group=band, music=lead, role="lead voice")
- Membership.objects.create(group=band, music=lead, role="bass player")
- m = GroupAdmin(Group, custom_site)
- request = self.factory.get("/group/", data={"members": lead.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- # There's only one Group instance
- self.assertEqual(cl.result_count, 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_distinct_for_through_m2m_at_second_level_in_list_filter(self):
- """
- When using a ManyToMany in list_filter at the second level behind a
- ForeignKey, distinct() must be called and results shouldn't appear more
- than once.
- """
- lead = Musician.objects.create(name="Vox")
- band = Group.objects.create(name="The Hype")
- Concert.objects.create(name="Woodstock", group=band)
- Membership.objects.create(group=band, music=lead, role="lead voice")
- Membership.objects.create(group=band, music=lead, role="bass player")
- m = ConcertAdmin(Concert, custom_site)
- request = self.factory.get("/concert/", data={"group__members": lead.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- # There's only one Concert instance
- self.assertEqual(cl.result_count, 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_distinct_for_inherited_m2m_in_list_filter(self):
- """
- Regression test for #13902: When using a ManyToMany in list_filter,
- results shouldn't appear more than once. Model managed in the
- admin inherits from the one that defines the relationship.
- """
- lead = Musician.objects.create(name="John")
- four = Quartet.objects.create(name="The Beatles")
- Membership.objects.create(group=four, music=lead, role="lead voice")
- Membership.objects.create(group=four, music=lead, role="guitar player")
- m = QuartetAdmin(Quartet, custom_site)
- request = self.factory.get("/quartet/", data={"members": lead.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- # There's only one Quartet instance
- self.assertEqual(cl.result_count, 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_distinct_for_m2m_to_inherited_in_list_filter(self):
- """
- Regression test for #13902: When using a ManyToMany in list_filter,
- results shouldn't appear more than once. Target of the relationship
- inherits from another.
- """
- lead = ChordsMusician.objects.create(name="Player A")
- three = ChordsBand.objects.create(name="The Chords Trio")
- Invitation.objects.create(band=three, player=lead, instrument="guitar")
- Invitation.objects.create(band=three, player=lead, instrument="bass")
- m = ChordsBandAdmin(ChordsBand, custom_site)
- request = self.factory.get("/chordsband/", data={"members": lead.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- # There's only one ChordsBand instance
- self.assertEqual(cl.result_count, 1)
- def test_distinct_for_non_unique_related_object_in_list_filter(self):
- """
- Regressions tests for #15819: If a field listed in list_filters
- is a non-unique related object, distinct() must be called.
- """
- parent = Parent.objects.create(name="Mary")
- # Two children with the same name
- Child.objects.create(parent=parent, name="Daniel")
- Child.objects.create(parent=parent, name="Daniel")
- m = ParentAdmin(Parent, custom_site)
- request = self.factory.get("/parent/", data={"child__name": "Daniel"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- # Make sure distinct() was called
- self.assertEqual(cl.queryset.count(), 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_changelist_search_form_validation(self):
- m = ConcertAdmin(Concert, custom_site)
- tests = [
- ({SEARCH_VAR: "\x00"}, "Null characters are not allowed."),
- ({SEARCH_VAR: "some\x00thing"}, "Null characters are not allowed."),
- ]
- for case, error in tests:
- with self.subTest(case=case):
- request = self.factory.get("/concert/", case)
- request.user = self.superuser
- request._messages = CookieStorage(request)
- m.get_changelist_instance(request)
- messages = [m.message for m in request._messages]
- self.assertEqual(1, len(messages))
- self.assertEqual(error, messages[0])
- def test_distinct_for_non_unique_related_object_in_search_fields(self):
- """
- Regressions tests for #15819: If a field listed in search_fields
- is a non-unique related object, distinct() must be called.
- """
- parent = Parent.objects.create(name="Mary")
- Child.objects.create(parent=parent, name="Danielle")
- Child.objects.create(parent=parent, name="Daniel")
- m = ParentAdmin(Parent, custom_site)
- request = self.factory.get("/parent/", data={SEARCH_VAR: "daniel"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- # Make sure distinct() was called
- self.assertEqual(cl.queryset.count(), 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_distinct_for_many_to_many_at_second_level_in_search_fields(self):
- """
- When using a ManyToMany in search_fields at the second level behind a
- ForeignKey, distinct() must be called and results shouldn't appear more
- than once.
- """
- lead = Musician.objects.create(name="Vox")
- band = Group.objects.create(name="The Hype")
- Concert.objects.create(name="Woodstock", group=band)
- Membership.objects.create(group=band, music=lead, role="lead voice")
- Membership.objects.create(group=band, music=lead, role="bass player")
- m = ConcertAdmin(Concert, custom_site)
- request = self.factory.get("/concert/", data={SEARCH_VAR: "vox"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- # There's only one Concert instance
- self.assertEqual(cl.queryset.count(), 1)
- # Queryset must be deletable.
- cl.queryset.delete()
- self.assertEqual(cl.queryset.count(), 0)
- def test_multiple_search_fields(self):
- """
- All rows containing each of the searched words are returned, where each
- word must be in one of search_fields.
- """
- band_duo = Group.objects.create(name="Duo")
- band_hype = Group.objects.create(name="The Hype")
- mary = Musician.objects.create(name="Mary Halvorson")
- jonathan = Musician.objects.create(name="Jonathan Finlayson")
- band_duo.members.set([mary, jonathan])
- Concert.objects.create(name="Tiny desk concert", group=band_duo)
- Concert.objects.create(name="Woodstock concert", group=band_hype)
- # FK lookup.
- concert_model_admin = ConcertAdmin(Concert, custom_site)
- concert_model_admin.search_fields = ["group__name", "name"]
- # Reverse FK lookup.
- group_model_admin = GroupAdmin(Group, custom_site)
- group_model_admin.search_fields = ["name", "concert__name", "members__name"]
- for search_string, result_count in (
- ("Duo Concert", 1),
- ("Tiny Desk Concert", 1),
- ("Concert", 2),
- ("Other Concert", 0),
- ("Duo Woodstock", 0),
- ):
- with self.subTest(search_string=search_string):
- # FK lookup.
- request = self.factory.get(
- "/concert/", data={SEARCH_VAR: search_string}
- )
- request.user = self.superuser
- concert_changelist = concert_model_admin.get_changelist_instance(
- request
- )
- self.assertEqual(concert_changelist.queryset.count(), result_count)
- # Reverse FK lookup.
- request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
- request.user = self.superuser
- group_changelist = group_model_admin.get_changelist_instance(request)
- self.assertEqual(group_changelist.queryset.count(), result_count)
- # Many-to-many lookup.
- for search_string, result_count in (
- ("Finlayson Duo Tiny", 1),
- ("Finlayson", 1),
- ("Finlayson Hype", 0),
- ("Jonathan Finlayson Duo", 1),
- ("Mary Jonathan Duo", 0),
- ("Oscar Finlayson Duo", 0),
- ):
- with self.subTest(search_string=search_string):
- request = self.factory.get("/group/", data={SEARCH_VAR: search_string})
- request.user = self.superuser
- group_changelist = group_model_admin.get_changelist_instance(request)
- self.assertEqual(group_changelist.queryset.count(), result_count)
- def test_pk_in_search_fields(self):
- band = Group.objects.create(name="The Hype")
- Concert.objects.create(name="Woodstock", group=band)
- m = ConcertAdmin(Concert, custom_site)
- m.search_fields = ["group__pk"]
- request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 1)
- request = self.factory.get("/concert/", data={SEARCH_VAR: band.pk + 5})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 0)
- def test_builtin_lookup_in_search_fields(self):
- band = Group.objects.create(name="The Hype")
- concert = Concert.objects.create(name="Woodstock", group=band)
- m = ConcertAdmin(Concert, custom_site)
- m.search_fields = ["name__iexact"]
- request = self.factory.get("/", data={SEARCH_VAR: "woodstock"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [concert])
- request = self.factory.get("/", data={SEARCH_VAR: "wood"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [])
- def test_custom_lookup_in_search_fields(self):
- band = Group.objects.create(name="The Hype")
- concert = Concert.objects.create(name="Woodstock", group=band)
- m = ConcertAdmin(Concert, custom_site)
- m.search_fields = ["group__name__cc"]
- with register_lookup(Field, Contains, lookup_name="cc"):
- request = self.factory.get("/", data={SEARCH_VAR: "Hype"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [concert])
- request = self.factory.get("/", data={SEARCH_VAR: "Woodstock"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [])
- def test_spanning_relations_with_custom_lookup_in_search_fields(self):
- hype = Group.objects.create(name="The Hype")
- concert = Concert.objects.create(name="Woodstock", group=hype)
- vox = Musician.objects.create(name="Vox", age=20)
- Membership.objects.create(music=vox, group=hype)
- # Register a custom lookup on IntegerField to ensure that field
- # traversing logic in ModelAdmin.get_search_results() works.
- with register_lookup(IntegerField, Exact, lookup_name="exactly"):
- m = ConcertAdmin(Concert, custom_site)
- m.search_fields = ["group__members__age__exactly"]
- request = self.factory.get("/", data={SEARCH_VAR: "20"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [concert])
- request = self.factory.get("/", data={SEARCH_VAR: "21"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [])
- def test_custom_lookup_with_pk_shortcut(self):
- self.assertEqual(CharPK._meta.pk.name, "char_pk") # Not equal to 'pk'.
- m = admin.ModelAdmin(CustomIdUser, custom_site)
- abc = CharPK.objects.create(char_pk="abc")
- abcd = CharPK.objects.create(char_pk="abcd")
- m = admin.ModelAdmin(CharPK, custom_site)
- m.search_fields = ["pk__exact"]
- request = self.factory.get("/", data={SEARCH_VAR: "abc"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [abc])
- request = self.factory.get("/", data={SEARCH_VAR: "abcd"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertCountEqual(cl.queryset, [abcd])
- def test_no_distinct_for_m2m_in_list_filter_without_params(self):
- """
- If a ManyToManyField is in list_filter but isn't in any lookup params,
- the changelist's query shouldn't have distinct.
- """
- m = BandAdmin(Band, custom_site)
- for lookup_params in ({}, {"name": "test"}):
- request = self.factory.get("/band/", lookup_params)
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertIs(cl.queryset.query.distinct, False)
- # A ManyToManyField in params does have distinct applied.
- request = self.factory.get("/band/", {"genres": "0"})
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- self.assertIs(cl.queryset.query.distinct, True)
- def test_pagination(self):
- """
- Regression tests for #12893: Pagination in admins changelist doesn't
- use queryset set by modeladmin.
- """
- parent = Parent.objects.create(name="anything")
- for i in range(1, 31):
- Child.objects.create(name="name %s" % i, parent=parent)
- Child.objects.create(name="filtered %s" % i, parent=parent)
- request = self.factory.get("/child/")
- request.user = self.superuser
- # Test default queryset
- m = ChildAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 60)
- self.assertEqual(cl.paginator.count, 60)
- self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6])
- # Test custom queryset
- m = FilteredChildAdmin(Child, custom_site)
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.queryset.count(), 30)
- self.assertEqual(cl.paginator.count, 30)
- self.assertEqual(list(cl.paginator.page_range), [1, 2, 3])
- def test_computed_list_display_localization(self):
- """
- Regression test for #13196: output of functions should be localized
- in the changelist.
- """
- self.client.force_login(self.superuser)
- event = Event.objects.create(date=datetime.date.today())
- response = self.client.get(reverse("admin:admin_changelist_event_changelist"))
- self.assertContains(response, formats.localize(event.date))
- self.assertNotContains(response, str(event.date))
- def test_dynamic_list_display(self):
- """
- Regression tests for #14206: dynamic list_display support.
- """
- parent = Parent.objects.create(name="parent")
- for i in range(10):
- Child.objects.create(name="child %s" % i, parent=parent)
- user_noparents = self._create_superuser("noparents")
- user_parents = self._create_superuser("parents")
- # Test with user 'noparents'
- m = custom_site.get_model_admin(Child)
- request = self._mocked_authenticated_request("/child/", user_noparents)
- response = m.changelist_view(request)
- self.assertNotContains(response, "Parent object")
- list_display = m.get_list_display(request)
- list_display_links = m.get_list_display_links(request, list_display)
- self.assertEqual(list_display, ["name", "age"])
- self.assertEqual(list_display_links, ["name"])
- # Test with user 'parents'
- m = DynamicListDisplayChildAdmin(Child, custom_site)
- request = self._mocked_authenticated_request("/child/", user_parents)
- response = m.changelist_view(request)
- self.assertContains(response, "Parent object")
- custom_site.unregister(Child)
- list_display = m.get_list_display(request)
- list_display_links = m.get_list_display_links(request, list_display)
- self.assertEqual(list_display, ("parent", "name", "age"))
- self.assertEqual(list_display_links, ["parent"])
- # Test default implementation
- custom_site.register(Child, ChildAdmin)
- m = custom_site.get_model_admin(Child)
- request = self._mocked_authenticated_request("/child/", user_noparents)
- response = m.changelist_view(request)
- self.assertContains(response, "Parent object")
- def test_show_all(self):
- parent = Parent.objects.create(name="anything")
- for i in range(1, 31):
- Child.objects.create(name="name %s" % i, parent=parent)
- Child.objects.create(name="filtered %s" % i, parent=parent)
- # Add "show all" parameter to request
- request = self.factory.get("/child/", data={ALL_VAR: ""})
- request.user = self.superuser
- # Test valid "show all" request (number of total objects is under max)
- m = ChildAdmin(Child, custom_site)
- m.list_max_show_all = 200
- # 200 is the max we'll pass to ChangeList
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- self.assertEqual(len(cl.result_list), 60)
- # Test invalid "show all" request (number of total objects over max)
- # falls back to paginated pages
- m = ChildAdmin(Child, custom_site)
- m.list_max_show_all = 30
- # 30 is the max we'll pass to ChangeList for this test
- cl = m.get_changelist_instance(request)
- cl.get_results(request)
- self.assertEqual(len(cl.result_list), 10)
- def test_dynamic_list_display_links(self):
- """
- Regression tests for #16257: dynamic list_display_links support.
- """
- parent = Parent.objects.create(name="parent")
- for i in range(1, 10):
- Child.objects.create(id=i, name="child %s" % i, parent=parent, age=i)
- m = DynamicListDisplayLinksChildAdmin(Child, custom_site)
- superuser = self._create_superuser("superuser")
- request = self._mocked_authenticated_request("/child/", superuser)
- response = m.changelist_view(request)
- for i in range(1, 10):
- link = reverse("admin:admin_changelist_child_change", args=(i,))
- self.assertContains(response, '<a href="%s">%s</a>' % (link, i))
- list_display = m.get_list_display(request)
- list_display_links = m.get_list_display_links(request, list_display)
- self.assertEqual(list_display, ("parent", "name", "age"))
- self.assertEqual(list_display_links, ["age"])
- def test_no_list_display_links(self):
- """#15185 -- Allow no links from the 'change list' view grid."""
- p = Parent.objects.create(name="parent")
- m = NoListDisplayLinksParentAdmin(Parent, custom_site)
- superuser = self._create_superuser("superuser")
- request = self._mocked_authenticated_request("/parent/", superuser)
- response = m.changelist_view(request)
- link = reverse("admin:admin_changelist_parent_change", args=(p.pk,))
- self.assertNotContains(response, '<a href="%s">' % link)
- def test_clear_all_filters_link(self):
- self.client.force_login(self.superuser)
- url = reverse("admin:auth_user_changelist")
- response = self.client.get(url)
- self.assertNotContains(response, "✖ Clear all filters")
- link = '<a href="%s">✖ Clear all filters</a>'
- for data, href in (
- ({"is_staff__exact": "0"}, "?"),
- (
- {"is_staff__exact": "0", "username__startswith": "test"},
- "?username__startswith=test",
- ),
- (
- {"is_staff__exact": "0", SEARCH_VAR: "test"},
- "?%s=test" % SEARCH_VAR,
- ),
- (
- {"is_staff__exact": "0", IS_POPUP_VAR: "id"},
- "?%s=id" % IS_POPUP_VAR,
- ),
- ):
- with self.subTest(data=data):
- response = self.client.get(url, data=data)
- self.assertContains(response, link % href)
- def test_clear_all_filters_link_callable_filter(self):
- self.client.force_login(self.superuser)
- url = reverse("admin:admin_changelist_band_changelist")
- response = self.client.get(url)
- self.assertNotContains(response, "✖ Clear all filters")
- link = '<a href="%s">✖ Clear all filters</a>'
- for data, href in (
- ({"nr_of_members_partition": "5"}, "?"),
- (
- {"nr_of_members_partition": "more", "name__startswith": "test"},
- "?name__startswith=test",
- ),
- (
- {"nr_of_members_partition": "5", IS_POPUP_VAR: "id"},
- "?%s=id" % IS_POPUP_VAR,
- ),
- ):
- with self.subTest(data=data):
- response = self.client.get(url, data=data)
- self.assertContains(response, link % href)
- def test_no_clear_all_filters_link(self):
- self.client.force_login(self.superuser)
- url = reverse("admin:auth_user_changelist")
- link = ">✖ Clear all filters</a>"
- for data in (
- {SEARCH_VAR: "test"},
- {ORDER_VAR: "-1"},
- {TO_FIELD_VAR: "id"},
- {PAGE_VAR: "1"},
- {IS_POPUP_VAR: "1"},
- {IS_FACETS_VAR: ""},
- {"username__startswith": "test"},
- ):
- with self.subTest(data=data):
- response = self.client.get(url, data=data)
- self.assertNotContains(response, link)
- def test_tuple_list_display(self):
- swallow = Swallow.objects.create(origin="Africa", load="12.34", speed="22.2")
- swallow2 = Swallow.objects.create(origin="Africa", load="12.34", speed="22.2")
- swallow_o2o = SwallowOneToOne.objects.create(swallow=swallow2)
- model_admin = SwallowAdmin(Swallow, custom_site)
- superuser = self._create_superuser("superuser")
- request = self._mocked_authenticated_request("/swallow/", superuser)
- response = model_admin.changelist_view(request)
- # just want to ensure it doesn't blow up during rendering
- self.assertContains(response, str(swallow.origin))
- self.assertContains(response, str(swallow.load))
- self.assertContains(response, str(swallow.speed))
- # Reverse one-to-one relations should work.
- self.assertContains(response, '<td class="field-swallowonetoone">-</td>')
- self.assertContains(
- response, '<td class="field-swallowonetoone">%s</td>' % swallow_o2o
- )
- def test_multiuser_edit(self):
- """
- Simultaneous edits of list_editable fields on the changelist by
- different users must not result in one user's edits creating a new
- object instead of modifying the correct existing object (#11313).
- """
- # To replicate this issue, simulate the following steps:
- # 1. User1 opens an admin changelist with list_editable fields.
- # 2. User2 edits object "Foo" such that it moves to another page in
- # the pagination order and saves.
- # 3. User1 edits object "Foo" and saves.
- # 4. The edit made by User1 does not get applied to object "Foo" but
- # instead is used to create a new object (bug).
- # For this test, order the changelist by the 'speed' attribute and
- # display 3 objects per page (SwallowAdmin.list_per_page = 3).
- # Setup the test to reflect the DB state after step 2 where User2 has
- # edited the first swallow object's speed from '4' to '1'.
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- c = Swallow.objects.create(origin="Swallow C", load=5, speed=5)
- d = Swallow.objects.create(origin="Swallow D", load=9, speed=9)
- superuser = self._create_superuser("superuser")
- self.client.force_login(superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- # Send the POST from User1 for step 3. It's still using the changelist
- # ordering from before User2's edits in step 2.
- data = {
- "form-TOTAL_FORMS": "3",
- "form-INITIAL_FORMS": "3",
- "form-MIN_NUM_FORMS": "0",
- "form-MAX_NUM_FORMS": "1000",
- "form-0-uuid": str(d.pk),
- "form-1-uuid": str(c.pk),
- "form-2-uuid": str(a.pk),
- "form-0-load": "9.0",
- "form-0-speed": "9.0",
- "form-1-load": "5.0",
- "form-1-speed": "5.0",
- "form-2-load": "5.0",
- "form-2-speed": "4.0",
- "_save": "Save",
- }
- response = self.client.post(
- changelist_url, data, follow=True, extra={"o": "-2"}
- )
- # The object User1 edited in step 3 is displayed on the changelist and
- # has the correct edits applied.
- self.assertContains(response, "1 swallow was changed successfully.")
- self.assertContains(response, a.origin)
- a.refresh_from_db()
- self.assertEqual(a.load, float(data["form-2-load"]))
- self.assertEqual(a.speed, float(data["form-2-speed"]))
- b.refresh_from_db()
- self.assertEqual(b.load, 2)
- self.assertEqual(b.speed, 2)
- c.refresh_from_db()
- self.assertEqual(c.load, float(data["form-1-load"]))
- self.assertEqual(c.speed, float(data["form-1-speed"]))
- d.refresh_from_db()
- self.assertEqual(d.load, float(data["form-0-load"]))
- self.assertEqual(d.speed, float(data["form-0-speed"]))
- # No new swallows were created.
- self.assertEqual(len(Swallow.objects.all()), 4)
- def test_get_edited_object_ids(self):
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- b = Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- c = Swallow.objects.create(origin="Swallow C", load=5, speed=5)
- superuser = self._create_superuser("superuser")
- self.client.force_login(superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- m = SwallowAdmin(Swallow, custom_site)
- data = {
- "form-TOTAL_FORMS": "3",
- "form-INITIAL_FORMS": "3",
- "form-MIN_NUM_FORMS": "0",
- "form-MAX_NUM_FORMS": "1000",
- "form-0-uuid": str(a.pk),
- "form-1-uuid": str(b.pk),
- "form-2-uuid": str(c.pk),
- "form-0-load": "9.0",
- "form-0-speed": "9.0",
- "form-1-load": "5.0",
- "form-1-speed": "5.0",
- "form-2-load": "5.0",
- "form-2-speed": "4.0",
- "_save": "Save",
- }
- request = self.factory.post(changelist_url, data=data)
- pks = m._get_edited_object_pks(request, prefix="form")
- self.assertEqual(sorted(pks), sorted([str(a.pk), str(b.pk), str(c.pk)]))
- def test_get_list_editable_queryset(self):
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- data = {
- "form-TOTAL_FORMS": "2",
- "form-INITIAL_FORMS": "2",
- "form-MIN_NUM_FORMS": "0",
- "form-MAX_NUM_FORMS": "1000",
- "form-0-uuid": str(a.pk),
- "form-0-load": "10",
- "_save": "Save",
- }
- superuser = self._create_superuser("superuser")
- self.client.force_login(superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- m = SwallowAdmin(Swallow, custom_site)
- request = self.factory.post(changelist_url, data=data)
- queryset = m._get_list_editable_queryset(request, prefix="form")
- self.assertEqual(queryset.count(), 1)
- data["form-0-uuid"] = "INVALD_PRIMARY_KEY"
- # The unfiltered queryset is returned if there's invalid data.
- request = self.factory.post(changelist_url, data=data)
- queryset = m._get_list_editable_queryset(request, prefix="form")
- self.assertEqual(queryset.count(), 2)
- def test_get_list_editable_queryset_with_regex_chars_in_prefix(self):
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- data = {
- "form$-TOTAL_FORMS": "2",
- "form$-INITIAL_FORMS": "2",
- "form$-MIN_NUM_FORMS": "0",
- "form$-MAX_NUM_FORMS": "1000",
- "form$-0-uuid": str(a.pk),
- "form$-0-load": "10",
- "_save": "Save",
- }
- superuser = self._create_superuser("superuser")
- self.client.force_login(superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- m = SwallowAdmin(Swallow, custom_site)
- request = self.factory.post(changelist_url, data=data)
- queryset = m._get_list_editable_queryset(request, prefix="form$")
- self.assertEqual(queryset.count(), 1)
- def test_changelist_view_list_editable_changed_objects_uses_filter(self):
- """list_editable edits use a filtered queryset to limit memory usage."""
- a = Swallow.objects.create(origin="Swallow A", load=4, speed=1)
- Swallow.objects.create(origin="Swallow B", load=2, speed=2)
- data = {
- "form-TOTAL_FORMS": "2",
- "form-INITIAL_FORMS": "2",
- "form-MIN_NUM_FORMS": "0",
- "form-MAX_NUM_FORMS": "1000",
- "form-0-uuid": str(a.pk),
- "form-0-load": "10",
- "_save": "Save",
- }
- superuser = self._create_superuser("superuser")
- self.client.force_login(superuser)
- changelist_url = reverse("admin:admin_changelist_swallow_changelist")
- with CaptureQueriesContext(connection) as context:
- response = self.client.post(changelist_url, data=data)
- self.assertEqual(response.status_code, 200)
- self.assertIn("WHERE", context.captured_queries[4]["sql"])
- self.assertIn("IN", context.captured_queries[4]["sql"])
- # Check only the first few characters since the UUID may have dashes.
- self.assertIn(str(a.pk)[:8], context.captured_queries[4]["sql"])
- def test_deterministic_order_for_unordered_model(self):
- """
- The primary key is used in the ordering of the changelist's results to
- guarantee a deterministic order, even when the model doesn't have any
- default ordering defined (#17198).
- """
- superuser = self._create_superuser("superuser")
- for counter in range(1, 51):
- UnorderedObject.objects.create(id=counter, bool=True)
- class UnorderedObjectAdmin(admin.ModelAdmin):
- list_per_page = 10
- def check_results_order(ascending=False):
- custom_site.register(UnorderedObject, UnorderedObjectAdmin)
- model_admin = UnorderedObjectAdmin(UnorderedObject, custom_site)
- counter = 0 if ascending else 51
- for page in range(1, 6):
- request = self._mocked_authenticated_request(
- "/unorderedobject/?p=%s" % page, superuser
- )
- response = model_admin.changelist_view(request)
- for result in response.context_data["cl"].result_list:
- counter += 1 if ascending else -1
- self.assertEqual(result.id, counter)
- custom_site.unregister(UnorderedObject)
- # When no order is defined at all, everything is ordered by '-pk'.
- check_results_order()
- # When an order field is defined but multiple records have the same
- # value for that field, make sure everything gets ordered by -pk as well.
- UnorderedObjectAdmin.ordering = ["bool"]
- check_results_order()
- # When order fields are defined, including the pk itself, use them.
- UnorderedObjectAdmin.ordering = ["bool", "-pk"]
- check_results_order()
- UnorderedObjectAdmin.ordering = ["bool", "pk"]
- check_results_order(ascending=True)
- UnorderedObjectAdmin.ordering = ["-id", "bool"]
- check_results_order()
- UnorderedObjectAdmin.ordering = ["id", "bool"]
- check_results_order(ascending=True)
- def test_deterministic_order_for_model_ordered_by_its_manager(self):
- """
- The primary key is used in the ordering of the changelist's results to
- guarantee a deterministic order, even when the model has a manager that
- defines a default ordering (#17198).
- """
- superuser = self._create_superuser("superuser")
- for counter in range(1, 51):
- OrderedObject.objects.create(id=counter, bool=True, number=counter)
- class OrderedObjectAdmin(admin.ModelAdmin):
- list_per_page = 10
- def check_results_order(ascending=False):
- custom_site.register(OrderedObject, OrderedObjectAdmin)
- model_admin = OrderedObjectAdmin(OrderedObject, custom_site)
- counter = 0 if ascending else 51
- for page in range(1, 6):
- request = self._mocked_authenticated_request(
- "/orderedobject/?p=%s" % page, superuser
- )
- response = model_admin.changelist_view(request)
- for result in response.context_data["cl"].result_list:
- counter += 1 if ascending else -1
- self.assertEqual(result.id, counter)
- custom_site.unregister(OrderedObject)
- # When no order is defined at all, use the model's default ordering
- # (i.e. 'number').
- check_results_order(ascending=True)
- # When an order field is defined but multiple records have the same
- # value for that field, make sure everything gets ordered by -pk as well.
- OrderedObjectAdmin.ordering = ["bool"]
- check_results_order()
- # When order fields are defined, including the pk itself, use them.
- OrderedObjectAdmin.ordering = ["bool", "-pk"]
- check_results_order()
- OrderedObjectAdmin.ordering = ["bool", "pk"]
- check_results_order(ascending=True)
- OrderedObjectAdmin.ordering = ["-id", "bool"]
- check_results_order()
- OrderedObjectAdmin.ordering = ["id", "bool"]
- check_results_order(ascending=True)
- @isolate_apps("admin_changelist")
- def test_total_ordering_optimization(self):
- class Related(models.Model):
- unique_field = models.BooleanField(unique=True)
- class Meta:
- ordering = ("unique_field",)
- class Model(models.Model):
- unique_field = models.BooleanField(unique=True)
- unique_nullable_field = models.BooleanField(unique=True, null=True)
- related = models.ForeignKey(Related, models.CASCADE)
- other_related = models.ForeignKey(Related, models.CASCADE)
- related_unique = models.OneToOneField(Related, models.CASCADE)
- field = models.BooleanField()
- other_field = models.BooleanField()
- null_field = models.BooleanField(null=True)
- class Meta:
- unique_together = {
- ("field", "other_field"),
- ("field", "null_field"),
- ("related", "other_related_id"),
- }
- class ModelAdmin(admin.ModelAdmin):
- def get_queryset(self, request):
- return Model.objects.none()
- request = self._mocked_authenticated_request("/", self.superuser)
- site = admin.AdminSite(name="admin")
- model_admin = ModelAdmin(Model, site)
- change_list = model_admin.get_changelist_instance(request)
- tests = (
- ([], ["-pk"]),
- # Unique non-nullable field.
- (["unique_field"], ["unique_field"]),
- (["-unique_field"], ["-unique_field"]),
- # Unique nullable field.
- (["unique_nullable_field"], ["unique_nullable_field", "-pk"]),
- # Field.
- (["field"], ["field", "-pk"]),
- # Related field introspection is not implemented.
- (["related__unique_field"], ["related__unique_field", "-pk"]),
- # Related attname unique.
- (["related_unique_id"], ["related_unique_id"]),
- # Related ordering introspection is not implemented.
- (["related_unique"], ["related_unique", "-pk"]),
- # Composite unique.
- (["field", "-other_field"], ["field", "-other_field"]),
- # Composite unique nullable.
- (["-field", "null_field"], ["-field", "null_field", "-pk"]),
- # Composite unique and nullable.
- (
- ["-field", "null_field", "other_field"],
- ["-field", "null_field", "other_field"],
- ),
- # Composite unique attnames.
- (["related_id", "-other_related_id"], ["related_id", "-other_related_id"]),
- # Composite unique names.
- (["related", "-other_related_id"], ["related", "-other_related_id", "-pk"]),
- )
- # F() objects composite unique.
- total_ordering = [F("field"), F("other_field").desc(nulls_last=True)]
- # F() objects composite unique nullable.
- non_total_ordering = [F("field"), F("null_field").desc(nulls_last=True)]
- tests += (
- (total_ordering, total_ordering),
- (non_total_ordering, non_total_ordering + ["-pk"]),
- )
- for ordering, expected in tests:
- with self.subTest(ordering=ordering):
- self.assertEqual(
- change_list._get_deterministic_ordering(ordering), expected
- )
- @isolate_apps("admin_changelist")
- def test_total_ordering_optimization_meta_constraints(self):
- class Related(models.Model):
- unique_field = models.BooleanField(unique=True)
- class Meta:
- ordering = ("unique_field",)
- class Model(models.Model):
- field_1 = models.BooleanField()
- field_2 = models.BooleanField()
- field_3 = models.BooleanField()
- field_4 = models.BooleanField()
- field_5 = models.BooleanField()
- field_6 = models.BooleanField()
- nullable_1 = models.BooleanField(null=True)
- nullable_2 = models.BooleanField(null=True)
- related_1 = models.ForeignKey(Related, models.CASCADE)
- related_2 = models.ForeignKey(Related, models.CASCADE)
- related_3 = models.ForeignKey(Related, models.CASCADE)
- related_4 = models.ForeignKey(Related, models.CASCADE)
- class Meta:
- constraints = [
- *[
- models.UniqueConstraint(fields=fields, name="".join(fields))
- for fields in (
- ["field_1"],
- ["nullable_1"],
- ["related_1"],
- ["related_2_id"],
- ["field_2", "field_3"],
- ["field_2", "nullable_2"],
- ["field_2", "related_3"],
- ["field_3", "related_4_id"],
- )
- ],
- models.CheckConstraint(check=models.Q(id__gt=0), name="foo"),
- models.UniqueConstraint(
- fields=["field_5"],
- condition=models.Q(id__gt=10),
- name="total_ordering_1",
- ),
- models.UniqueConstraint(
- fields=["field_6"],
- condition=models.Q(),
- name="total_ordering",
- ),
- ]
- class ModelAdmin(admin.ModelAdmin):
- def get_queryset(self, request):
- return Model.objects.none()
- request = self._mocked_authenticated_request("/", self.superuser)
- site = admin.AdminSite(name="admin")
- model_admin = ModelAdmin(Model, site)
- change_list = model_admin.get_changelist_instance(request)
- tests = (
- # Unique non-nullable field.
- (["field_1"], ["field_1"]),
- # Unique nullable field.
- (["nullable_1"], ["nullable_1", "-pk"]),
- # Related attname unique.
- (["related_1_id"], ["related_1_id"]),
- (["related_2_id"], ["related_2_id"]),
- # Related ordering introspection is not implemented.
- (["related_1"], ["related_1", "-pk"]),
- # Composite unique.
- (["-field_2", "field_3"], ["-field_2", "field_3"]),
- # Composite unique nullable.
- (["field_2", "-nullable_2"], ["field_2", "-nullable_2", "-pk"]),
- # Composite unique and nullable.
- (
- ["field_2", "-nullable_2", "field_3"],
- ["field_2", "-nullable_2", "field_3"],
- ),
- # Composite field and related field name.
- (["field_2", "-related_3"], ["field_2", "-related_3", "-pk"]),
- (["field_3", "related_4"], ["field_3", "related_4", "-pk"]),
- # Composite field and related field attname.
- (["field_2", "related_3_id"], ["field_2", "related_3_id"]),
- (["field_3", "-related_4_id"], ["field_3", "-related_4_id"]),
- # Partial unique constraint is ignored.
- (["field_5"], ["field_5", "-pk"]),
- # Unique constraint with an empty condition.
- (["field_6"], ["field_6"]),
- )
- for ordering, expected in tests:
- with self.subTest(ordering=ordering):
- self.assertEqual(
- change_list._get_deterministic_ordering(ordering), expected
- )
- def test_dynamic_list_filter(self):
- """
- Regression tests for ticket #17646: dynamic list_filter support.
- """
- parent = Parent.objects.create(name="parent")
- for i in range(10):
- Child.objects.create(name="child %s" % i, parent=parent)
- user_noparents = self._create_superuser("noparents")
- user_parents = self._create_superuser("parents")
- # Test with user 'noparents'
- m = DynamicListFilterChildAdmin(Child, custom_site)
- request = self._mocked_authenticated_request("/child/", user_noparents)
- response = m.changelist_view(request)
- self.assertEqual(response.context_data["cl"].list_filter, ["name", "age"])
- # Test with user 'parents'
- m = DynamicListFilterChildAdmin(Child, custom_site)
- request = self._mocked_authenticated_request("/child/", user_parents)
- response = m.changelist_view(request)
- self.assertEqual(
- response.context_data["cl"].list_filter, ("parent", "name", "age")
- )
- def test_dynamic_search_fields(self):
- child = self._create_superuser("child")
- m = DynamicSearchFieldsChildAdmin(Child, custom_site)
- request = self._mocked_authenticated_request("/child/", child)
- response = m.changelist_view(request)
- self.assertEqual(response.context_data["cl"].search_fields, ("name", "age"))
- def test_pagination_page_range(self):
- """
- Regression tests for ticket #15653: ensure the number of pages
- generated for changelist views are correct.
- """
- # instantiating and setting up ChangeList object
- m = GroupAdmin(Group, custom_site)
- request = self.factory.get("/group/")
- request.user = self.superuser
- cl = m.get_changelist_instance(request)
- cl.list_per_page = 10
- ELLIPSIS = cl.paginator.ELLIPSIS
- for number, pages, expected in [
- (1, 1, []),
- (1, 2, [1, 2]),
- (6, 11, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
- (6, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
- (6, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, ELLIPSIS, 12, 13]),
- (7, 12, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
- (7, 13, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
- (7, 14, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ELLIPSIS, 13, 14]),
- (8, 13, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13]),
- (8, 14, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]),
- (8, 15, [1, 2, ELLIPSIS, 5, 6, 7, 8, 9, 10, 11, ELLIPSIS, 14, 15]),
- ]:
- with self.subTest(number=number, pages=pages):
- # assuming exactly `pages * cl.list_per_page` objects
- Group.objects.all().delete()
- for i in range(pages * cl.list_per_page):
- Group.objects.create(name="test band")
- # setting page number and calculating page range
- cl.page_num = number
- cl.get_results(request)
- self.assertEqual(list(pagination(cl)["page_range"]), expected)
- def test_object_tools_displayed_no_add_permission(self):
- """
- When ModelAdmin.has_add_permission() returns False, the object-tools
- block is still shown.
- """
- superuser = self._create_superuser("superuser")
- m = EventAdmin(Event, custom_site)
- request = self._mocked_authenticated_request("/event/", superuser)
- self.assertFalse(m.has_add_permission(request))
- response = m.changelist_view(request)
- self.assertIn('<ul class="object-tools">', response.rendered_content)
- # The "Add" button inside the object-tools shouldn't appear.
- self.assertNotIn("Add ", response.rendered_content)
- def test_search_help_text(self):
- superuser = self._create_superuser("superuser")
- m = BandAdmin(Band, custom_site)
- # search_fields without search_help_text.
- m.search_fields = ["name"]
- request = self._mocked_authenticated_request("/band/", superuser)
- response = m.changelist_view(request)
- self.assertIsNone(response.context_data["cl"].search_help_text)
- self.assertNotContains(response, '<div class="help id="searchbar_helptext">')
- # search_fields with search_help_text.
- m.search_help_text = "Search help text"
- request = self._mocked_authenticated_request("/band/", superuser)
- response = m.changelist_view(request)
- self.assertEqual(
- response.context_data["cl"].search_help_text, "Search help text"
- )
- self.assertContains(
- response, '<div class="help" id="searchbar_helptext">Search help text</div>'
- )
- self.assertContains(
- response,
- '<input type="text" size="40" name="q" value="" id="searchbar" '
- 'aria-describedby="searchbar_helptext">',
- )
- def test_search_role(self):
- m = BandAdmin(Band, custom_site)
- m.search_fields = ["name"]
- request = self._mocked_authenticated_request("/band/", self.superuser)
- response = m.changelist_view(request)
- self.assertContains(
- response,
- '<form id="changelist-search" method="get" role="search">',
- )
- def test_search_bar_total_link_preserves_options(self):
- self.client.force_login(self.superuser)
- url = reverse("admin:auth_user_changelist")
- for data, href in (
- ({"is_staff__exact": "0"}, "?"),
- ({"is_staff__exact": "0", IS_POPUP_VAR: "1"}, f"?{IS_POPUP_VAR}=1"),
- ({"is_staff__exact": "0", IS_FACETS_VAR: ""}, f"?{IS_FACETS_VAR}"),
- (
- {"is_staff__exact": "0", IS_POPUP_VAR: "1", IS_FACETS_VAR: ""},
- f"?{IS_POPUP_VAR}=1&{IS_FACETS_VAR}",
- ),
- ):
- with self.subTest(data=data):
- response = self.client.get(url, data=data)
- self.assertContains(
- response, f'0 results (<a href="{href}">1 total</a>)'
- )
- def test_list_display_related_field(self):
- parent = Parent.objects.create(name="I am your father")
- child = Child.objects.create(name="I am your child", parent=parent)
- GrandChild.objects.create(name="I am your grandchild", parent=child)
- request = self._mocked_authenticated_request("/grandchild/", self.superuser)
- m = GrandChildAdmin(GrandChild, custom_site)
- response = m.changelist_view(request)
- self.assertContains(response, parent.name)
- self.assertContains(response, child.name)
- def test_list_display_related_field_null(self):
- GrandChild.objects.create(name="I am parentless", parent=None)
- request = self._mocked_authenticated_request("/grandchild/", self.superuser)
- m = GrandChildAdmin(GrandChild, custom_site)
- response = m.changelist_view(request)
- self.assertContains(response, '<td class="field-parent__name">-</td>')
- self.assertContains(response, '<td class="field-parent__parent__name">-</td>')
- def test_list_display_related_field_ordering(self):
- parent_a = Parent.objects.create(name="Alice")
- parent_z = Parent.objects.create(name="Zara")
- Child.objects.create(name="Alice's child", parent=parent_a)
- Child.objects.create(name="Zara's child", parent=parent_z)
- class ChildAdmin(admin.ModelAdmin):
- list_display = ["name", "parent__name"]
- list_per_page = 1
- m = ChildAdmin(Child, custom_site)
- # Order ascending.
- request = self._mocked_authenticated_request("/grandchild/?o=1", self.superuser)
- response = m.changelist_view(request)
- self.assertContains(response, parent_a.name)
- self.assertNotContains(response, parent_z.name)
- # Order descending.
- request = self._mocked_authenticated_request(
- "/grandchild/?o=-1", self.superuser
- )
- response = m.changelist_view(request)
- self.assertNotContains(response, parent_a.name)
- self.assertContains(response, parent_z.name)
- def test_list_display_related_field_ordering_fields(self):
- class ChildAdmin(admin.ModelAdmin):
- list_display = ["name", "parent__name"]
- ordering = ["parent__name"]
- m = ChildAdmin(Child, custom_site)
- request = self._mocked_authenticated_request("/", self.superuser)
- cl = m.get_changelist_instance(request)
- self.assertEqual(cl.get_ordering_field_columns(), {2: "asc"})
- class GetAdminLogTests(TestCase):
- def test_custom_user_pk_not_named_id(self):
- """
- {% get_admin_log %} works if the user model's primary key isn't named
- 'id'.
- """
- context = Context(
- {
- "user": CustomIdUser(),
- "log_entries": LogEntry.objects.all(),
- }
- )
- template = Template(
- "{% load log %}{% get_admin_log 10 as admin_log for_user user %}"
- )
- # This template tag just logs.
- self.assertEqual(template.render(context), "")
- def test_no_user(self):
- """{% get_admin_log %} works without specifying a user."""
- user = User(username="jondoe", password="secret", email="super@example.com")
- user.save()
- LogEntry.objects.log_actions(user.pk, [user], 1, single_object=True)
- context = Context({"log_entries": LogEntry.objects.all()})
- t = Template(
- "{% load log %}"
- "{% get_admin_log 100 as admin_log %}"
- "{% for entry in admin_log %}"
- "{{ entry|safe }}"
- "{% endfor %}"
- )
- self.assertEqual(t.render(context), "Added “jondoe”.")
- def test_missing_args(self):
- msg = "'get_admin_log' statements require two arguments"
- with self.assertRaisesMessage(TemplateSyntaxError, msg):
- Template("{% load log %}{% get_admin_log 10 as %}")
- def test_non_integer_limit(self):
- msg = "First argument to 'get_admin_log' must be an integer"
- with self.assertRaisesMessage(TemplateSyntaxError, msg):
- Template(
- '{% load log %}{% get_admin_log "10" as admin_log for_user user %}'
- )
- def test_without_as(self):
- msg = "Second argument to 'get_admin_log' must be 'as'"
- with self.assertRaisesMessage(TemplateSyntaxError, msg):
- Template("{% load log %}{% get_admin_log 10 ad admin_log for_user user %}")
- def test_without_for_user(self):
- msg = "Fourth argument to 'get_admin_log' must be 'for_user'"
- with self.assertRaisesMessage(TemplateSyntaxError, msg):
- Template("{% load log %}{% get_admin_log 10 as admin_log foruser user %}")
- @override_settings(ROOT_URLCONF="admin_changelist.urls")
- class SeleniumTests(AdminSeleniumTestCase):
- available_apps = ["admin_changelist"] + AdminSeleniumTestCase.available_apps
- def setUp(self):
- User.objects.create_superuser(username="super", password="secret", email=None)
- def test_add_row_selection(self):
- """
- The status line for selected rows gets updated correctly (#22038).
- """
- from selenium.webdriver.common.by import By
- self.admin_login(username="super", password="secret")
- self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
- form_id = "#changelist-form"
- # Test amount of rows in the Changelist
- rows = self.selenium.find_elements(
- By.CSS_SELECTOR, "%s #result_list tbody tr" % form_id
- )
- self.assertEqual(len(rows), 1)
- row = rows[0]
- selection_indicator = self.selenium.find_element(
- By.CSS_SELECTOR, "%s .action-counter" % form_id
- )
- all_selector = self.selenium.find_element(By.ID, "action-toggle")
- row_selector = self.selenium.find_element(
- By.CSS_SELECTOR,
- "%s #result_list tbody tr:first-child .action-select" % form_id,
- )
- # Test current selection
- self.assertEqual(selection_indicator.text, "0 of 1 selected")
- self.assertIs(all_selector.get_property("checked"), False)
- self.assertEqual(row.get_attribute("class"), "")
- # Select a row and check again
- row_selector.click()
- self.assertEqual(selection_indicator.text, "1 of 1 selected")
- self.assertIs(all_selector.get_property("checked"), True)
- self.assertEqual(row.get_attribute("class"), "selected")
- # Deselect a row and check again
- row_selector.click()
- self.assertEqual(selection_indicator.text, "0 of 1 selected")
- self.assertIs(all_selector.get_property("checked"), False)
- self.assertEqual(row.get_attribute("class"), "")
- def test_modifier_allows_multiple_section(self):
- """
- Selecting a row and then selecting another row whilst holding shift
- should select all rows in-between.
- """
- from selenium.webdriver.common.action_chains import ActionChains
- from selenium.webdriver.common.by import By
- from selenium.webdriver.common.keys import Keys
- Parent.objects.bulk_create([Parent(name="parent%d" % i) for i in range(5)])
- self.admin_login(username="super", password="secret")
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- checkboxes = self.selenium.find_elements(
- By.CSS_SELECTOR, "tr input.action-select"
- )
- self.assertEqual(len(checkboxes), 5)
- for c in checkboxes:
- self.assertIs(c.get_property("checked"), False)
- # Check first row. Hold-shift and check next-to-last row.
- checkboxes[0].click()
- ActionChains(self.selenium).key_down(Keys.SHIFT).click(checkboxes[-2]).key_up(
- Keys.SHIFT
- ).perform()
- for c in checkboxes[:-2]:
- self.assertIs(c.get_property("checked"), True)
- self.assertIs(checkboxes[-1].get_property("checked"), False)
- def test_selection_counter_is_synced_when_page_is_shown(self):
- from selenium.webdriver.common.by import By
- self.admin_login(username="super", password="secret")
- self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
- form_id = "#changelist-form"
- first_row_checkbox_selector = (
- f"{form_id} #result_list tbody tr:first-child .action-select"
- )
- selection_indicator_selector = f"{form_id} .action-counter"
- selection_indicator = self.selenium.find_element(
- By.CSS_SELECTOR, selection_indicator_selector
- )
- row_checkbox = self.selenium.find_element(
- By.CSS_SELECTOR, first_row_checkbox_selector
- )
- # Select a row.
- row_checkbox.click()
- self.assertEqual(selection_indicator.text, "1 of 1 selected")
- # Go to another page and get back.
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- self.selenium.back()
- # The selection indicator is synced with the selected checkboxes.
- selection_indicator = self.selenium.find_element(
- By.CSS_SELECTOR, selection_indicator_selector
- )
- row_checkbox = self.selenium.find_element(
- By.CSS_SELECTOR, first_row_checkbox_selector
- )
- selected_rows = 1 if row_checkbox.is_selected() else 0
- self.assertEqual(selection_indicator.text, f"{selected_rows} of 1 selected")
- def test_select_all_across_pages(self):
- from selenium.webdriver.common.by import By
- Parent.objects.bulk_create([Parent(name="parent%d" % i) for i in range(101)])
- self.admin_login(username="super", password="secret")
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- selection_indicator = self.selenium.find_element(
- By.CSS_SELECTOR, ".action-counter"
- )
- select_all_indicator = self.selenium.find_element(
- By.CSS_SELECTOR, ".actions .all"
- )
- question = self.selenium.find_element(By.CSS_SELECTOR, ".actions > .question")
- clear = self.selenium.find_element(By.CSS_SELECTOR, ".actions > .clear")
- select_all = self.selenium.find_element(By.ID, "action-toggle")
- select_across = self.selenium.find_elements(By.NAME, "select_across")
- self.assertIs(question.is_displayed(), False)
- self.assertIs(clear.is_displayed(), False)
- self.assertIs(select_all.get_property("checked"), False)
- for hidden_input in select_across:
- self.assertEqual(hidden_input.get_property("value"), "0")
- self.assertIs(selection_indicator.is_displayed(), True)
- self.assertEqual(selection_indicator.text, "0 of 100 selected")
- self.assertIs(select_all_indicator.is_displayed(), False)
- select_all.click()
- self.assertIs(question.is_displayed(), True)
- self.assertIs(clear.is_displayed(), False)
- self.assertIs(select_all.get_property("checked"), True)
- for hidden_input in select_across:
- self.assertEqual(hidden_input.get_property("value"), "0")
- self.assertIs(selection_indicator.is_displayed(), True)
- self.assertEqual(selection_indicator.text, "100 of 100 selected")
- self.assertIs(select_all_indicator.is_displayed(), False)
- question.click()
- self.assertIs(question.is_displayed(), False)
- self.assertIs(clear.is_displayed(), True)
- self.assertIs(select_all.get_property("checked"), True)
- for hidden_input in select_across:
- self.assertEqual(hidden_input.get_property("value"), "1")
- self.assertIs(selection_indicator.is_displayed(), False)
- self.assertIs(select_all_indicator.is_displayed(), True)
- clear.click()
- self.assertIs(question.is_displayed(), False)
- self.assertIs(clear.is_displayed(), False)
- self.assertIs(select_all.get_property("checked"), False)
- for hidden_input in select_across:
- self.assertEqual(hidden_input.get_property("value"), "0")
- self.assertIs(selection_indicator.is_displayed(), True)
- self.assertEqual(selection_indicator.text, "0 of 100 selected")
- self.assertIs(select_all_indicator.is_displayed(), False)
- def test_actions_warn_on_pending_edits(self):
- from selenium.webdriver.common.by import By
- Parent.objects.create(name="foo")
- self.admin_login(username="super", password="secret")
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- name_input = self.selenium.find_element(By.ID, "id_form-0-name")
- name_input.clear()
- name_input.send_keys("bar")
- self.selenium.find_element(By.ID, "action-toggle").click()
- self.selenium.find_element(By.NAME, "index").click() # Go
- alert = self.selenium.switch_to.alert
- try:
- self.assertEqual(
- alert.text,
- "You have unsaved changes on individual editable fields. If you "
- "run an action, your unsaved changes will be lost.",
- )
- finally:
- alert.dismiss()
- def test_save_with_changes_warns_on_pending_action(self):
- from selenium.webdriver.common.by import By
- from selenium.webdriver.support.ui import Select
- Parent.objects.create(name="parent")
- self.admin_login(username="super", password="secret")
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- name_input = self.selenium.find_element(By.ID, "id_form-0-name")
- name_input.clear()
- name_input.send_keys("other name")
- Select(self.selenium.find_element(By.NAME, "action")).select_by_value(
- "delete_selected"
- )
- self.selenium.find_element(By.NAME, "_save").click()
- alert = self.selenium.switch_to.alert
- try:
- self.assertEqual(
- alert.text,
- "You have selected an action, but you haven’t saved your "
- "changes to individual fields yet. Please click OK to save. "
- "You’ll need to re-run the action.",
- )
- finally:
- alert.dismiss()
- def test_save_without_changes_warns_on_pending_action(self):
- from selenium.webdriver.common.by import By
- from selenium.webdriver.support.ui import Select
- Parent.objects.create(name="parent")
- self.admin_login(username="super", password="secret")
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_parent_changelist")
- )
- Select(self.selenium.find_element(By.NAME, "action")).select_by_value(
- "delete_selected"
- )
- self.selenium.find_element(By.NAME, "_save").click()
- alert = self.selenium.switch_to.alert
- try:
- self.assertEqual(
- alert.text,
- "You have selected an action, and you haven’t made any "
- "changes on individual fields. You’re probably looking for "
- "the Go button rather than the Save button.",
- )
- finally:
- alert.dismiss()
- def test_collapse_filters(self):
- from selenium.webdriver.common.by import By
- self.admin_login(username="super", password="secret")
- self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
- # The UserAdmin has 3 field filters by default: "staff status",
- # "superuser status", and "active".
- details = self.selenium.find_elements(By.CSS_SELECTOR, "details")
- # All filters are opened at first.
- for detail in details:
- self.assertTrue(detail.get_attribute("open"))
- # Collapse "staff' and "superuser" filters.
- for detail in details[:2]:
- summary = detail.find_element(By.CSS_SELECTOR, "summary")
- summary.click()
- self.assertFalse(detail.get_attribute("open"))
- # Filters are in the same state after refresh.
- self.selenium.refresh()
- self.assertFalse(
- self.selenium.find_element(
- By.CSS_SELECTOR, "[data-filter-title='staff status']"
- ).get_attribute("open")
- )
- self.assertFalse(
- self.selenium.find_element(
- By.CSS_SELECTOR, "[data-filter-title='superuser status']"
- ).get_attribute("open")
- )
- self.assertTrue(
- self.selenium.find_element(
- By.CSS_SELECTOR, "[data-filter-title='active']"
- ).get_attribute("open")
- )
- # Collapse a filter on another view (Bands).
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_band_changelist")
- )
- self.selenium.find_element(By.CSS_SELECTOR, "summary").click()
- # Go to Users view and then, back again to Bands view.
- self.selenium.get(self.live_server_url + reverse("admin:auth_user_changelist"))
- self.selenium.get(
- self.live_server_url + reverse("admin:admin_changelist_band_changelist")
- )
- # The filter remains in the same state.
- self.assertFalse(
- self.selenium.find_element(
- By.CSS_SELECTOR,
- "[data-filter-title='number of members']",
- ).get_attribute("open")
- )
- def test_collapse_filter_with_unescaped_title(self):
- from selenium.webdriver.common.by import By
- self.admin_login(username="super", password="secret")
- changelist_url = reverse("admin:admin_changelist_proxyuser_changelist")
- self.selenium.get(self.live_server_url + changelist_url)
- # Title is escaped.
- filter_title = self.selenium.find_element(
- By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']"
- )
- filter_title.find_element(By.CSS_SELECTOR, "summary").click()
- self.assertFalse(filter_title.get_attribute("open"))
- # Filter is in the same state after refresh.
- self.selenium.refresh()
- self.assertFalse(
- self.selenium.find_element(
- By.CSS_SELECTOR, "[data-filter-title='It\\'s OK']"
- ).get_attribute("open")
- )
|