import datetime import json import os import re import unittest from urllib.parse import parse_qsl, urljoin, urlparse import pytz from django.contrib.admin import AdminSite, ModelAdmin from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.models import ADDITION, DELETION, LogEntry from django.contrib.admin.options import TO_FIELD_VAR from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.tests import AdminSeleniumTestCase from django.contrib.admin.utils import quote from django.contrib.admin.views.main import IS_POPUP_VAR from django.contrib.auth import REDIRECT_FIELD_NAME, get_permission_codename from django.contrib.auth.models import Group, Permission, User from django.contrib.contenttypes.models import ContentType from django.contrib.staticfiles.storage import staticfiles_storage from django.core import mail from django.core.checks import Error from django.core.files import temp as tempfile from django.forms.utils import ErrorList from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.test import ( SimpleTestCase, TestCase, ignore_warnings, modify_settings, override_settings, skipUnlessDBFeature, ) from django.test.utils import override_script_prefix, patch_logger from django.urls import NoReverseMatch, resolve, reverse from django.utils import formats, translation from django.utils.cache import get_max_age from django.utils.deprecation import RemovedInDjango21Warning from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.html import escape from django.utils.http import urlencode from . import customadmin from .admin import CityAdmin, site, site2 from .forms import MediaActionForm from .models import ( Actor, AdminOrderedAdminMethod, AdminOrderedCallable, AdminOrderedField, AdminOrderedModelMethod, Answer, Answer2, Article, BarAccount, Book, Bookmark, Category, Chapter, ChapterXtra1, ChapterXtra2, Character, Child, Choice, City, Collector, Color, ComplexSortedPerson, CoverLetter, CustomArticle, CyclicOne, CyclicTwo, DooHickey, Employee, EmptyModel, ExternalSubscriber, Fabric, FancyDoodad, FieldOverridePost, FilteredManager, FooAccount, FoodDelivery, FunkyTag, Gallery, Grommet, Inquisition, Language, Link, MainPrepopulated, Media, ModelWithStringPrimaryKey, OtherStory, Paper, Parent, ParentWithDependentChildren, ParentWithUUIDPK, Person, Persona, Picture, Pizza, Plot, PlotDetails, PluggableSearchPerson, Podcast, Post, PrePopulatedPost, Promo, Question, Recommendation, Recommender, RelatedPrepopulated, RelatedWithUUIDPKModel, Report, Restaurant, RowLevelChangePermissionModel, SecretHideout, Section, ShortMessage, Simple, State, Story, Subscriber, SuperSecretHideout, SuperVillain, Telegram, TitleTranslation, Topping, UnchangeableObject, UndeletableObject, UnorderedObject, Villain, Vodcast, Whatsit, Widget, Worker, WorkHour, ) ERROR_MESSAGE = "Please enter the correct username and password \ for a staff account. Note that both fields may be case-sensitive." class AdminFieldExtractionMixin: """ Helper methods for extracting data from AdminForm. """ def get_admin_form_fields(self, response): """ Return a list of AdminFields for the AdminForm in the response. """ admin_form = response.context['adminform'] fieldsets = list(admin_form) field_lines = [] for fieldset in fieldsets: field_lines += list(fieldset) fields = [] for field_line in field_lines: fields += list(field_line) return fields def get_admin_readonly_fields(self, response): """ Return the readonly fields for the response's AdminForm. """ return [f for f in self.get_admin_form_fields(response) if f.is_readonly] def get_admin_readonly_field(self, response, field_name): """ Return the readonly field for the given field_name. """ admin_readonly_fields = self.get_admin_readonly_fields(response) for field in admin_readonly_fields: if field.field['name'] == field_name: return field @override_settings(ROOT_URLCONF='admin_views.urls', USE_I18N=True, USE_L10N=False, LANGUAGE_CODE='en') class AdminViewBasicTestCase(TestCase): @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.s1 = Section.objects.create(name='Test section') cls.a1 = Article.objects.create( content='

Middle content

', date=datetime.datetime(2008, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a2 = Article.objects.create( content='

Oldest content

', date=datetime.datetime(2000, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a3 = Article.objects.create( content='

Newest content

', date=datetime.datetime(2009, 3, 18, 11, 54, 58), section=cls.s1 ) cls.p1 = PrePopulatedPost.objects.create(title='A Long Title', published=True, slug='a-long-title') cls.color1 = Color.objects.create(value='Red', warm=True) cls.color2 = Color.objects.create(value='Orange', warm=True) cls.color3 = Color.objects.create(value='Blue', warm=False) cls.color4 = Color.objects.create(value='Green', warm=False) cls.fab1 = Fabric.objects.create(surface='x') cls.fab2 = Fabric.objects.create(surface='y') cls.fab3 = Fabric.objects.create(surface='plain') cls.b1 = Book.objects.create(name='Book 1') cls.b2 = Book.objects.create(name='Book 2') cls.pro1 = Promo.objects.create(name='Promo 1', book=cls.b1) cls.pro1 = Promo.objects.create(name='Promo 2', book=cls.b2) cls.chap1 = Chapter.objects.create(title='Chapter 1', content='[ insert contents here ]', book=cls.b1) cls.chap2 = Chapter.objects.create(title='Chapter 2', content='[ insert contents here ]', book=cls.b1) cls.chap3 = Chapter.objects.create(title='Chapter 1', content='[ insert contents here ]', book=cls.b2) cls.chap4 = Chapter.objects.create(title='Chapter 2', content='[ insert contents here ]', book=cls.b2) cls.cx1 = ChapterXtra1.objects.create(chap=cls.chap1, xtra='ChapterXtra1 1') cls.cx2 = ChapterXtra1.objects.create(chap=cls.chap3, xtra='ChapterXtra1 2') # Post data for edit inline cls.inline_post_data = { "name": "Test section", # inline data "article_set-TOTAL_FORMS": "6", "article_set-INITIAL_FORMS": "3", "article_set-MAX_NUM_FORMS": "0", "article_set-0-id": cls.a1.pk, # there is no title in database, give one here or formset will fail. "article_set-0-title": "Norske bostaver æøå skaper problemer", "article_set-0-content": "<p>Middle content</p>", "article_set-0-date_0": "2008-03-18", "article_set-0-date_1": "11:54:58", "article_set-0-section": cls.s1.pk, "article_set-1-id": cls.a2.pk, "article_set-1-title": "Need a title.", "article_set-1-content": "<p>Oldest content</p>", "article_set-1-date_0": "2000-03-18", "article_set-1-date_1": "11:54:58", "article_set-2-id": cls.a3.pk, "article_set-2-title": "Need a title.", "article_set-2-content": "<p>Newest content</p>", "article_set-2-date_0": "2009-03-18", "article_set-2-date_1": "11:54:58", "article_set-3-id": "", "article_set-3-title": "", "article_set-3-content": "", "article_set-3-date_0": "", "article_set-3-date_1": "", "article_set-4-id": "", "article_set-4-title": "", "article_set-4-content": "", "article_set-4-date_0": "", "article_set-4-date_1": "", "article_set-5-id": "", "article_set-5-title": "", "article_set-5-content": "", "article_set-5-date_0": "", "article_set-5-date_1": "", } def setUp(self): self.client.force_login(self.superuser) def assertContentBefore(self, response, text1, text2, failing_msg=None): """ Testing utility asserting that text1 appears before text2 in response content. """ self.assertEqual(response.status_code, 200) self.assertLess( response.content.index(force_bytes(text1)), response.content.index(force_bytes(text2)), (failing_msg or '') + '\nResponse:\n' + response.content.decode(response.charset) ) class AdminViewBasicTest(AdminViewBasicTestCase): def test_trailing_slash_required(self): """ If you leave off the trailing slash, app should redirect and add it. """ add_url = reverse('admin:admin_views_article_add') response = self.client.get(add_url[:-1]) self.assertRedirects(response, add_url, status_code=301) def test_admin_static_template_tag(self): """ admin_static.static points to the collectstatic version (as django.contrib.collectstatic is in INSTALLED_APPS). """ old_url = staticfiles_storage.base_url staticfiles_storage.base_url = '/test/' try: self.assertEqual(static('path'), '/test/path') finally: staticfiles_storage.base_url = old_url def test_basic_add_GET(self): """ A smoke test to ensure GET on the add_view works. """ response = self.client.get(reverse('admin:admin_views_section_add')) self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) def test_add_with_GET_args(self): response = self.client.get(reverse('admin:admin_views_section_add'), {'name': 'My Section'}) self.assertContains( response, 'value="My Section"', msg_prefix="Couldn't find an input with the right value in the response" ) def test_basic_edit_GET(self): """ A smoke test to ensure GET on the change_view works. """ response = self.client.get(reverse('admin:admin_views_section_change', args=(self.s1.pk,))) self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) def test_basic_edit_GET_string_PK(self): """ GET on the change_view (when passing a string as the PK argument for a model with an integer PK field) redirects to the index page with a message saying the object doesn't exist. """ response = self.client.get(reverse('admin:admin_views_section_change', args=(quote("abc/"),)), follow=True) self.assertRedirects(response, reverse('admin:index')) self.assertEqual( [m.message for m in response.context['messages']], ["""section with ID "abc/" doesn't exist. Perhaps it was deleted?"""] ) def test_basic_edit_GET_old_url_redirect(self): """ The change URL changed in Django 1.9, but the old one still redirects. """ response = self.client.get( reverse('admin:admin_views_section_change', args=(self.s1.pk,)).replace('change/', '') ) self.assertRedirects(response, reverse('admin:admin_views_section_change', args=(self.s1.pk,))) def test_basic_inheritance_GET_string_PK(self): """ GET on the change_view (for inherited models) redirects to the index page with a message saying the object doesn't exist. """ response = self.client.get(reverse('admin:admin_views_supervillain_change', args=('abc',)), follow=True) self.assertRedirects(response, reverse('admin:index')) self.assertEqual( [m.message for m in response.context['messages']], ["""super villain with ID "abc" doesn't exist. Perhaps it was deleted?"""] ) def test_basic_add_POST(self): """ A smoke test to ensure POST on add_view works. """ post_data = { "name": "Another Section", # inline data "article_set-TOTAL_FORMS": "3", "article_set-INITIAL_FORMS": "0", "article_set-MAX_NUM_FORMS": "0", } response = self.client.post(reverse('admin:admin_views_section_add'), post_data) self.assertEqual(response.status_code, 302) # redirect somewhere def test_popup_add_POST(self): """ Ensure http response from a popup is properly escaped. """ post_data = { '_popup': '1', 'title': 'title with a new\nline', 'content': 'some content', 'date_0': '2010-09-10', 'date_1': '14:55:39', } response = self.client.post(reverse('admin:admin_views_article_add'), post_data) self.assertContains(response, 'title with a new\\nline') def test_basic_edit_POST(self): """ A smoke test to ensure POST on edit_view works. """ url = reverse('admin:admin_views_section_change', args=(self.s1.pk,)) response = self.client.post(url, self.inline_post_data) self.assertEqual(response.status_code, 302) # redirect somewhere def test_edit_save_as(self): """ Test "save as". """ post_data = self.inline_post_data.copy() post_data.update({ '_saveasnew': 'Save+as+new', "article_set-1-section": "1", "article_set-2-section": "1", "article_set-3-section": "1", "article_set-4-section": "1", "article_set-5-section": "1", }) response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), post_data) self.assertEqual(response.status_code, 302) # redirect somewhere def test_edit_save_as_delete_inline(self): """ Should be able to "Save as new" while also deleting an inline. """ post_data = self.inline_post_data.copy() post_data.update({ '_saveasnew': 'Save+as+new', "article_set-1-section": "1", "article_set-2-section": "1", "article_set-2-DELETE": "1", "article_set-3-section": "1", }) response = self.client.post(reverse('admin:admin_views_section_change', args=(self.s1.pk,)), post_data) self.assertEqual(response.status_code, 302) # started with 3 articles, one was deleted. self.assertEqual(Section.objects.latest('id').article_set.count(), 2) def test_change_list_column_field_classes(self): response = self.client.get(reverse('admin:admin_views_article_changelist')) # callables display the callable name. self.assertContains(response, 'column-callable_year') self.assertContains(response, 'field-callable_year') # lambdas display as "lambda" + index that they appear in list_display. self.assertContains(response, 'column-lambda8') self.assertContains(response, 'field-lambda8') def test_change_list_sorting_callable(self): """ Ensure we can sort on a list_display field that is a callable (column 2 is callable_year in ArticleAdmin) """ response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': 2}) self.assertContentBefore( response, 'Oldest content', 'Middle content', "Results of sorting on callable are out of order." ) self.assertContentBefore( response, 'Middle content', 'Newest content', "Results of sorting on callable are out of order." ) def test_change_list_sorting_model(self): """ Ensure we can sort on a list_display field that is a Model method (column 3 is 'model_year' in ArticleAdmin) """ response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '-3'}) self.assertContentBefore( response, 'Newest content', 'Middle content', "Results of sorting on Model method are out of order." ) self.assertContentBefore( response, 'Middle content', 'Oldest content', "Results of sorting on Model method are out of order." ) def test_change_list_sorting_model_admin(self): """ Ensure we can sort on a list_display field that is a ModelAdmin method (column 4 is 'modeladmin_year' in ArticleAdmin) """ response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '4'}) self.assertContentBefore( response, 'Oldest content', 'Middle content', "Results of sorting on ModelAdmin method are out of order." ) self.assertContentBefore( response, 'Middle content', 'Newest content', "Results of sorting on ModelAdmin method are out of order." ) def test_change_list_sorting_model_admin_reverse(self): """ Ensure we can sort on a list_display field that is a ModelAdmin method in reverse order (i.e. admin_order_field uses the '-' prefix) (column 6 is 'model_year_reverse' in ArticleAdmin) """ response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '6'}) self.assertContentBefore( response, '2009', '2008', "Results of sorting on ModelAdmin method are out of order." ) self.assertContentBefore( response, '2008', '2000', "Results of sorting on ModelAdmin method are out of order." ) # Let's make sure the ordering is right and that we don't get a # FieldError when we change to descending order response = self.client.get(reverse('admin:admin_views_article_changelist'), {'o': '-6'}) self.assertContentBefore( response, '2000', '2008', "Results of sorting on ModelAdmin method are out of order." ) self.assertContentBefore( response, '2008', '2009', "Results of sorting on ModelAdmin method are out of order." ) def test_change_list_sorting_multiple(self): p1 = Person.objects.create(name="Chris", gender=1, alive=True) p2 = Person.objects.create(name="Chris", gender=2, alive=True) p3 = Person.objects.create(name="Bob", gender=1, alive=True) link1 = reverse('admin:admin_views_person_change', args=(p1.pk,)) link2 = reverse('admin:admin_views_person_change', args=(p2.pk,)) link3 = reverse('admin:admin_views_person_change', args=(p3.pk,)) # Sort by name, gender response = self.client.get(reverse('admin:admin_views_person_changelist'), {'o': '1.2'}) self.assertContentBefore(response, link3, link1) self.assertContentBefore(response, link1, link2) # Sort by gender descending, name response = self.client.get(reverse('admin:admin_views_person_changelist'), {'o': '-2.1'}) self.assertContentBefore(response, link2, link3) self.assertContentBefore(response, link3, link1) def test_change_list_sorting_preserve_queryset_ordering(self): """ If no ordering is defined in `ModelAdmin.ordering` or in the query string, then the underlying order of the queryset should not be changed, even if it is defined in `Modeladmin.get_queryset()`. Refs #11868, #7309. """ p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80) p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70) p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60) link1 = reverse('admin:admin_views_person_change', args=(p1.pk,)) link2 = reverse('admin:admin_views_person_change', args=(p2.pk,)) link3 = reverse('admin:admin_views_person_change', args=(p3.pk,)) response = self.client.get(reverse('admin:admin_views_person_changelist'), {}) self.assertContentBefore(response, link3, link2) self.assertContentBefore(response, link2, link1) def test_change_list_sorting_model_meta(self): # Test ordering on Model Meta is respected l1 = Language.objects.create(iso='ur', name='Urdu') l2 = Language.objects.create(iso='ar', name='Arabic') link1 = reverse('admin:admin_views_language_change', args=(quote(l1.pk),)) link2 = reverse('admin:admin_views_language_change', args=(quote(l2.pk),)) response = self.client.get(reverse('admin:admin_views_language_changelist'), {}) self.assertContentBefore(response, link2, link1) # Test we can override with query string response = self.client.get(reverse('admin:admin_views_language_changelist'), {'o': '-1'}) self.assertContentBefore(response, link1, link2) def test_change_list_sorting_override_model_admin(self): # Test ordering on Model Admin is respected, and overrides Model Meta dt = datetime.datetime.now() p1 = Podcast.objects.create(name="A", release_date=dt) p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10)) link1 = reverse('admin:admin_views_podcast_change', args=(p1.pk,)) link2 = reverse('admin:admin_views_podcast_change', args=(p2.pk,)) response = self.client.get(reverse('admin:admin_views_podcast_changelist'), {}) self.assertContentBefore(response, link1, link2) def test_multiple_sort_same_field(self): # The changelist displays the correct columns if two columns correspond # to the same ordering field. dt = datetime.datetime.now() p1 = Podcast.objects.create(name="A", release_date=dt) p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10)) link1 = reverse('admin:admin_views_podcast_change', args=(quote(p1.pk),)) link2 = reverse('admin:admin_views_podcast_change', args=(quote(p2.pk),)) response = self.client.get(reverse('admin:admin_views_podcast_changelist'), {}) self.assertContentBefore(response, link1, link2) p1 = ComplexSortedPerson.objects.create(name="Bob", age=10) p2 = ComplexSortedPerson.objects.create(name="Amy", age=20) link1 = reverse('admin:admin_views_complexsortedperson_change', args=(p1.pk,)) link2 = reverse('admin:admin_views_complexsortedperson_change', args=(p2.pk,)) response = self.client.get(reverse('admin:admin_views_complexsortedperson_changelist'), {}) # Should have 5 columns (including action checkbox col) self.assertContains(response, '_id fields in list display.""" state = State.objects.create(name='Karnataka') City.objects.create(state=state, name='Bangalore') response = self.client.get(reverse('admin:admin_views_city_changelist'), {}) response.context['cl'].list_display = ['id', 'name', 'state'] self.assertIs(response.context['cl'].has_related_field_in_list_display(), True) response.context['cl'].list_display = ['id', 'name', 'state_id'] self.assertIs(response.context['cl'].has_related_field_in_list_display(), False) def test_has_related_field_in_list_display_o2o(self): """Joins shouldn't be performed for _id fields in list display.""" media = Media.objects.create(name='Foo') Vodcast.objects.create(media=media) response = self.client.get(reverse('admin:admin_views_vodcast_changelist'), {}) response.context['cl'].list_display = ['media'] self.assertIs(response.context['cl'].has_related_field_in_list_display(), True) response.context['cl'].list_display = ['media_id'] self.assertIs(response.context['cl'].has_related_field_in_list_display(), False) def test_limited_filter(self): """Ensure admin changelist filters do not contain objects excluded via limit_choices_to. This also tests relation-spanning filters (e.g. 'color__value'). """ response = self.client.get(reverse('admin:admin_views_thing_changelist')) self.assertContains( response, '
', msg_prefix="Expected filter not found in changelist view" ) self.assertNotContains( response, 'Blue', msg_prefix="Changelist filter not correctly limited by limit_choices_to" ) def test_relation_spanning_filters(self): changelist_url = reverse('admin:admin_views_chapterxtra1_changelist') response = self.client.get(changelist_url) self.assertContains(response, '
') filters = { 'chap__id__exact': { 'values': [c.id for c in Chapter.objects.all()], 'test': lambda obj, value: obj.chap.id == value, }, 'chap__title': { 'values': [c.title for c in Chapter.objects.all()], 'test': lambda obj, value: obj.chap.title == value, }, 'chap__book__id__exact': { 'values': [b.id for b in Book.objects.all()], 'test': lambda obj, value: obj.chap.book.id == value, }, 'chap__book__name': { 'values': [b.name for b in Book.objects.all()], 'test': lambda obj, value: obj.chap.book.name == value, }, 'chap__book__promo__id__exact': { 'values': [p.id for p in Promo.objects.all()], 'test': lambda obj, value: obj.chap.book.promo_set.filter(id=value).exists(), }, 'chap__book__promo__name': { 'values': [p.name for p in Promo.objects.all()], 'test': lambda obj, value: obj.chap.book.promo_set.filter(name=value).exists(), }, } for filter_path, params in filters.items(): for value in params['values']: query_string = urlencode({filter_path: value}) # ensure filter link exists self.assertContains(response, '' % reverse('admin:logout')) self.assertContains(response, '' % reverse('admin:password_change')) def test_named_group_field_choices_change_list(self): """ Ensures the admin changelist shows correct values in the relevant column for rows corresponding to instances of a model in which a named group has been used in the choices option of a field. """ link1 = reverse('admin:admin_views_fabric_change', args=(self.fab1.pk,)) link2 = reverse('admin:admin_views_fabric_change', args=(self.fab2.pk,)) response = self.client.get(reverse('admin:admin_views_fabric_changelist')) fail_msg = ( "Changelist table isn't showing the right human-readable values " "set by a model field 'choices' option named group." ) self.assertContains(response, 'Horizontal' % link1, msg_prefix=fail_msg, html=True) self.assertContains(response, 'Vertical' % link2, msg_prefix=fail_msg, html=True) def test_named_group_field_choices_filter(self): """ Ensures the filter UI shows correctly when at least one named group has been used in the choices option of a model field. """ response = self.client.get(reverse('admin:admin_views_fabric_changelist')) fail_msg = ( "Changelist filter isn't showing options contained inside a model " "field 'choices' option named group." ) self.assertContains(response, '
') self.assertContains( response, 'Horizontal', msg_prefix=fail_msg, html=True ) self.assertContains( response, 'Vertical', msg_prefix=fail_msg, html=True ) def test_change_list_null_boolean_display(self): Post.objects.create(public=None) response = self.client.get(reverse('admin:admin_views_post_changelist')) self.assertContains(response, 'icon-unknown.svg') def test_i18n_language_non_english_default(self): """ Check if the JavaScript i18n view returns an empty language catalog if the default language is non-English but the selected language is English. See #13388 and #3594 for more details. """ with self.settings(LANGUAGE_CODE='fr'), translation.override('en-us'): response = self.client.get(reverse('admin:jsi18n')) self.assertNotContains(response, 'Choisir une heure') def test_i18n_language_non_english_fallback(self): """ Makes sure that the fallback language is still working properly in cases where the selected language cannot be found. """ with self.settings(LANGUAGE_CODE='fr'), translation.override('none'): response = self.client.get(reverse('admin:jsi18n')) self.assertContains(response, 'Choisir une heure') def test_jsi18n_with_context(self): response = self.client.get(reverse('admin-extra-context:jsi18n')) self.assertEqual(response.status_code, 200) def test_L10N_deactivated(self): """ Check if L10N is deactivated, the JavaScript i18n view doesn't return localized date/time formats. Refs #14824. """ with self.settings(LANGUAGE_CODE='ru', USE_L10N=False), translation.override('none'): response = self.client.get(reverse('admin:jsi18n')) self.assertNotContains(response, '%d.%m.%Y %H:%M:%S') self.assertContains(response, '%Y-%m-%d %H:%M:%S') def test_disallowed_filtering(self): with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as calls: response = self.client.get( "%s?owner__email__startswith=fuzzy" % reverse('admin:admin_views_album_changelist') ) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) # Filters are allowed if explicitly included in list_filter response = self.client.get("%s?color__value__startswith=red" % reverse('admin:admin_views_thing_changelist')) self.assertEqual(response.status_code, 200) response = self.client.get("%s?color__value=red" % reverse('admin:admin_views_thing_changelist')) self.assertEqual(response.status_code, 200) # Filters should be allowed if they involve a local field without the # need to whitelist them in list_filter or date_hierarchy. response = self.client.get("%s?age__gt=30" % reverse('admin:admin_views_person_changelist')) self.assertEqual(response.status_code, 200) e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123') e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124') WorkHour.objects.create(datum=datetime.datetime.now(), employee=e1) WorkHour.objects.create(datum=datetime.datetime.now(), employee=e2) response = self.client.get(reverse('admin:admin_views_workhour_changelist')) self.assertContains(response, 'employee__person_ptr__exact') response = self.client.get("%s?employee__person_ptr__exact=%d" % ( reverse('admin:admin_views_workhour_changelist'), e1.pk) ) self.assertEqual(response.status_code, 200) def test_disallowed_to_field(self): with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: url = reverse('admin:admin_views_section_changelist') response = self.client.get(url, {TO_FIELD_VAR: 'missing_field'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) # Specifying a field that is not referred by any other model registered # to this admin site should raise an exception. with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: response = self.client.get(reverse('admin:admin_views_section_changelist'), {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) # #23839 - Primary key should always be allowed, even if the referenced model isn't registered. response = self.client.get(reverse('admin:admin_views_notreferenced_changelist'), {TO_FIELD_VAR: 'id'}) self.assertEqual(response.status_code, 200) # #23915 - Specifying a field referenced by another model though a m2m should be allowed. response = self.client.get(reverse('admin:admin_views_recipe_changelist'), {TO_FIELD_VAR: 'rname'}) self.assertEqual(response.status_code, 200) # #23604, #23915 - Specifying a field referenced through a reverse m2m relationship should be allowed. response = self.client.get(reverse('admin:admin_views_ingredient_changelist'), {TO_FIELD_VAR: 'iname'}) self.assertEqual(response.status_code, 200) # #23329 - Specifying a field that is not referred by any other model directly registered # to this admin site but registered through inheritance should be allowed. response = self.client.get(reverse('admin:admin_views_referencedbyparent_changelist'), {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 200) # #23431 - Specifying a field that is only referred to by a inline of a registered # model should be allowed. response = self.client.get(reverse('admin:admin_views_referencedbyinline_changelist'), {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 200) # #25622 - Specifying a field of a model only referred by a generic # relation should raise DisallowedModelAdminToField. url = reverse('admin:admin_views_referencedbygenrel_changelist') with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: response = self.client.get(url, {TO_FIELD_VAR: 'object_id'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) # We also want to prevent the add, change, and delete views from # leaking a disallowed field value. with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: response = self.client.post(reverse('admin:admin_views_section_add'), {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) section = Section.objects.create() with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: url = reverse('admin:admin_views_section_change', args=(section.pk,)) response = self.client.post(url, {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) with patch_logger('django.security.DisallowedModelAdminToField', 'error') as calls: url = reverse('admin:admin_views_section_delete', args=(section.pk,)) response = self.client.post(url, {TO_FIELD_VAR: 'name'}) self.assertEqual(response.status_code, 400) self.assertEqual(len(calls), 1) def test_allowed_filtering_15103(self): """ Regressions test for ticket 15103 - filtering on fields defined in a ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields can break. """ # Filters should be allowed if they are defined on a ForeignKey pointing to this model url = "%s?leader__name=Palin&leader__age=27" % reverse('admin:admin_views_inquisition_changelist') response = self.client.get(url) self.assertEqual(response.status_code, 200) def test_popup_dismiss_related(self): """ Regression test for ticket 20664 - ensure the pk is properly quoted. """ actor = Actor.objects.create(name="Palin", age=27) response = self.client.get("%s?%s" % (reverse('admin:admin_views_actor_changelist'), IS_POPUP_VAR)) self.assertContains(response, 'data-popup-opener="%s"' % actor.pk) def test_hide_change_password(self): """ Tests if the "change password" link in the admin is hidden if the User does not have a usable password set. (against 9bea85795705d015cdadc82c68b99196a8554f5c) """ user = User.objects.get(username='super') user.set_unusable_password() user.save() self.client.force_login(user) response = self.client.get(reverse('admin:index')) self.assertNotContains( response, reverse('admin:password_change'), msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.' ) def test_change_view_with_show_delete_extra_context(self): """ The 'show_delete' context variable in the admin's change view controls the display of the delete button. """ instance = UndeletableObject.objects.create(name='foo') response = self.client.get(reverse('admin:admin_views_undeletableobject_change', args=(instance.pk,))) self.assertNotContains(response, 'deletelink') def test_allows_attributeerror_to_bubble_up(self): """ AttributeErrors are allowed to bubble when raised inside a change list view. Requires a model to be created so there's something to display. Refs: #16655, #18593, and #18747 """ Simple.objects.create() with self.assertRaises(AttributeError): self.client.get(reverse('admin:admin_views_simple_changelist')) def test_changelist_with_no_change_url(self): """ ModelAdmin.changelist_view shouldn't result in a NoReverseMatch if url for change_view is removed from get_urls (#20934). """ UnchangeableObject.objects.create() response = self.client.get(reverse('admin:admin_views_unchangeableobject_changelist')) self.assertEqual(response.status_code, 200) # Check the format of the shown object -- shouldn't contain a change link self.assertContains(response, 'UnchangeableObject object', html=True) def test_invalid_appindex_url(self): """ #21056 -- URL reversing shouldn't work for nonexistent apps. """ good_url = '/test_admin/admin/admin_views/' confirm_good_url = reverse('admin:app_list', kwargs={'app_label': 'admin_views'}) self.assertEqual(good_url, confirm_good_url) with self.assertRaises(NoReverseMatch): reverse('admin:app_list', kwargs={'app_label': 'this_should_fail'}) with self.assertRaises(NoReverseMatch): reverse('admin:app_list', args=('admin_views2',)) def test_resolve_admin_views(self): index_match = resolve('/test_admin/admin4/') list_match = resolve('/test_admin/admin4/auth/user/') self.assertIs(index_match.func.admin_site, customadmin.simple_site) self.assertIsInstance(list_match.func.model_admin, customadmin.CustomPwdTemplateUserAdmin) def test_adminsite_display_site_url(self): """ #13749 - Admin should display link to front-end site 'View site' """ url = reverse('admin:index') response = self.client.get(url) self.assertEqual(response.context['site_url'], '/my-site-url/') self.assertContains(response, 'View site') @override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True) def test_date_hierarchy_timezone_dst(self): # This datetime doesn't exist in this timezone due to DST. date = pytz.timezone('America/Sao_Paulo').localize(datetime.datetime(2016, 10, 16, 15), is_dst=None) q = Question.objects.create(question='Why?', expires=date) Answer2.objects.create(question=q, answer='Because.') response = self.client.get(reverse('admin:admin_views_answer2_changelist')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'question__expires__day=16') self.assertContains(response, 'question__expires__month=10') self.assertContains(response, 'question__expires__year=2016') @override_settings(TEMPLATES=[{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', # Put this app's and the shared tests templates dirs in DIRS to take precedence # over the admin's templates dir. 'DIRS': [ os.path.join(os.path.dirname(__file__), 'templates'), os.path.join(os.path.dirname(os.path.dirname(__file__)), 'templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }]) class AdminCustomTemplateTests(AdminViewBasicTestCase): def test_custom_model_admin_templates(self): # Test custom change list template with custom extra context response = self.client.get(reverse('admin:admin_views_customarticle_changelist')) self.assertContains(response, "var hello = 'Hello!';") self.assertTemplateUsed(response, 'custom_admin/change_list.html') # Test custom add form template response = self.client.get(reverse('admin:admin_views_customarticle_add')) self.assertTemplateUsed(response, 'custom_admin/add_form.html') # Add an article so we can test delete, change, and history views post = self.client.post(reverse('admin:admin_views_customarticle_add'), { 'content': '

great article

', 'date_0': '2008-03-18', 'date_1': '10:54:39' }) self.assertRedirects(post, reverse('admin:admin_views_customarticle_changelist')) self.assertEqual(CustomArticle.objects.all().count(), 1) article_pk = CustomArticle.objects.all()[0].pk # Test custom delete, change, and object history templates # Test custom change form template response = self.client.get(reverse('admin:admin_views_customarticle_change', args=(article_pk,))) self.assertTemplateUsed(response, 'custom_admin/change_form.html') response = self.client.get(reverse('admin:admin_views_customarticle_delete', args=(article_pk,))) self.assertTemplateUsed(response, 'custom_admin/delete_confirmation.html') response = self.client.post(reverse('admin:admin_views_customarticle_changelist'), data={ 'index': 0, 'action': ['delete_selected'], '_selected_action': ['1'], }) self.assertTemplateUsed(response, 'custom_admin/delete_selected_confirmation.html') response = self.client.get(reverse('admin:admin_views_customarticle_history', args=(article_pk,))) self.assertTemplateUsed(response, 'custom_admin/object_history.html') # A custom popup response template may be specified by # ModelAdmin.popup_response_template. response = self.client.post(reverse('admin:admin_views_customarticle_add') + '?%s=1' % IS_POPUP_VAR, { 'content': '

great article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', IS_POPUP_VAR: '1' }) self.assertEqual(response.template_name, 'custom_admin/popup_response.html') def test_extended_bodyclass_template_change_form(self): """ The admin/change_form.html template uses block.super in the bodyclass block. """ response = self.client.get(reverse('admin:admin_views_section_add')) self.assertContains(response, 'bodyclass_consistency_check ') def test_change_password_template(self): user = User.objects.get(username='super') response = self.client.get(reverse('admin:auth_user_password_change', args=(user.id,))) # The auth/user/change_password.html template uses super in the # bodyclass block. self.assertContains(response, 'bodyclass_consistency_check ') # When a site has multiple passwords in the browser's password manager, # a browser pop up asks which user the new password is for. To prevent # this, the username is added to the change password form. self.assertContains(response, '') def test_extended_bodyclass_template_index(self): """ The admin/index.html template uses block.super in the bodyclass block. """ response = self.client.get(reverse('admin:index')) self.assertContains(response, 'bodyclass_consistency_check ') def test_extended_bodyclass_change_list(self): """ The admin/change_list.html' template uses block.super in the bodyclass block. """ response = self.client.get(reverse('admin:admin_views_article_changelist')) self.assertContains(response, 'bodyclass_consistency_check ') def test_extended_bodyclass_template_login(self): """ The admin/login.html template uses block.super in the bodyclass block. """ self.client.logout() response = self.client.get(reverse('admin:login')) self.assertContains(response, 'bodyclass_consistency_check ') def test_extended_bodyclass_template_delete_confirmation(self): """ The admin/delete_confirmation.html template uses block.super in the bodyclass block. """ group = Group.objects.create(name="foogroup") response = self.client.get(reverse('admin:auth_group_delete', args=(group.id,))) self.assertContains(response, 'bodyclass_consistency_check ') def test_extended_bodyclass_template_delete_selected_confirmation(self): """ The admin/delete_selected_confirmation.html template uses block.super in bodyclass block. """ group = Group.objects.create(name="foogroup") post_data = { 'action': 'delete_selected', 'selected_across': '0', 'index': '0', '_selected_action': group.id } response = self.client.post(reverse('admin:auth_group_changelist'), post_data) self.assertEqual(response.context['site_header'], 'Django administration') self.assertContains(response, 'bodyclass_consistency_check ') def test_filter_with_custom_template(self): """ A custom template can be used to render an admin filter. """ response = self.client.get(reverse('admin:admin_views_color2_changelist')) self.assertTemplateUsed(response, 'custom_filter_template.html') @override_settings(ROOT_URLCONF='admin_views.urls') class AdminViewFormUrlTest(TestCase): current_app = "admin3" @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.s1 = Section.objects.create(name='Test section') cls.a1 = Article.objects.create( content='

Middle content

', date=datetime.datetime(2008, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a2 = Article.objects.create( content='

Oldest content

', date=datetime.datetime(2000, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a3 = Article.objects.create( content='

Newest content

', date=datetime.datetime(2009, 3, 18, 11, 54, 58), section=cls.s1 ) cls.p1 = PrePopulatedPost.objects.create(title='A Long Title', published=True, slug='a-long-title') def setUp(self): self.client.force_login(self.superuser) def test_change_form_URL_has_correct_value(self): """ change_view has form_url in response.context """ response = self.client.get( reverse('admin:admin_views_section_change', args=(self.s1.pk,), current_app=self.current_app) ) self.assertIn('form_url', response.context, msg='form_url not present in response.context') self.assertEqual(response.context['form_url'], 'pony') def test_initial_data_can_be_overridden(self): """ The behavior for setting initial form data can be overridden in the ModelAdmin class. Usually, the initial value is set via the GET params. """ response = self.client.get( reverse('admin:admin_views_restaurant_add', current_app=self.current_app), {'name': 'test_value'} ) # this would be the usual behaviour self.assertNotContains(response, 'value="test_value"') # this is the overridden behaviour self.assertContains(response, 'value="overridden_value"') @override_settings(ROOT_URLCONF='admin_views.urls') class AdminJavaScriptTest(TestCase): @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') def setUp(self): self.client.force_login(self.superuser) def test_js_minified_only_if_debug_is_false(self): """ The minified versions of the JS files are only used when DEBUG is False. """ with override_settings(DEBUG=False): response = self.client.get(reverse('admin:admin_views_section_add')) self.assertNotContains(response, 'vendor/jquery/jquery.js') self.assertContains(response, 'vendor/jquery/jquery.min.js') self.assertNotContains(response, 'prepopulate.js') self.assertContains(response, 'prepopulate.min.js') self.assertNotContains(response, 'actions.js') self.assertContains(response, 'actions.min.js') self.assertNotContains(response, 'collapse.js') self.assertContains(response, 'collapse.min.js') self.assertNotContains(response, 'inlines.js') self.assertContains(response, 'inlines.min.js') with override_settings(DEBUG=True): response = self.client.get(reverse('admin:admin_views_section_add')) self.assertContains(response, 'vendor/jquery/jquery.js') self.assertNotContains(response, 'vendor/jquery/jquery.min.js') self.assertContains(response, 'prepopulate.js') self.assertNotContains(response, 'prepopulate.min.js') self.assertContains(response, 'actions.js') self.assertNotContains(response, 'actions.min.js') self.assertContains(response, 'collapse.js') self.assertNotContains(response, 'collapse.min.js') self.assertContains(response, 'inlines.js') self.assertNotContains(response, 'inlines.min.js') @override_settings(ROOT_URLCONF='admin_views.urls') class SaveAsTests(TestCase): @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.per1 = Person.objects.create(name='John Mauchly', gender=1, alive=True) def setUp(self): self.client.force_login(self.superuser) def test_save_as_duplication(self): """'save as' creates a new person""" post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42} response = self.client.post(reverse('admin:admin_views_person_change', args=(self.per1.pk,)), post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=self.per1.pk)), 1) new_person = Person.objects.latest('id') self.assertRedirects(response, reverse('admin:admin_views_person_change', args=(new_person.pk,))) def test_save_as_continue_false(self): """ Saving a new object using "Save as new" redirects to the changelist instead of the change view when ModelAdmin.save_as_continue=False. """ post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42} url = reverse('admin:admin_views_person_change', args=(self.per1.pk,), current_app=site2.name) response = self.client.post(url, post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=self.per1.pk)), 1) self.assertRedirects(response, reverse('admin:admin_views_person_changelist', current_app=site2.name)) def test_save_as_new_with_validation_errors(self): """ When you click "Save as new" and have a validation error, you only see the "Save as new" button and not the other save buttons, and that only the "Save as" button is visible. """ response = self.client.post(reverse('admin:admin_views_person_change', args=(self.per1.pk,)), { '_saveasnew': '', 'gender': 'invalid', '_addanother': 'fail', }) self.assertContains(response, 'Please correct the errors below.') self.assertFalse(response.context['show_save_and_add_another']) self.assertFalse(response.context['show_save_and_continue']) self.assertTrue(response.context['show_save_as_new']) def test_save_as_new_with_validation_errors_with_inlines(self): parent = Parent.objects.create(name='Father') child = Child.objects.create(parent=parent, name='Child') response = self.client.post(reverse('admin:admin_views_parent_change', args=(parent.pk,)), { '_saveasnew': 'Save as new', 'child_set-0-parent': parent.pk, 'child_set-0-id': child.pk, 'child_set-0-name': 'Child', 'child_set-INITIAL_FORMS': 1, 'child_set-MAX_NUM_FORMS': 1000, 'child_set-MIN_NUM_FORMS': 0, 'child_set-TOTAL_FORMS': 4, 'name': '_invalid', }) self.assertContains(response, 'Please correct the error below.') self.assertFalse(response.context['show_save_and_add_another']) self.assertFalse(response.context['show_save_and_continue']) self.assertTrue(response.context['show_save_as_new']) def test_save_as_new_with_inlines_with_validation_errors(self): parent = Parent.objects.create(name='Father') child = Child.objects.create(parent=parent, name='Child') response = self.client.post(reverse('admin:admin_views_parent_change', args=(parent.pk,)), { '_saveasnew': 'Save as new', 'child_set-0-parent': parent.pk, 'child_set-0-id': child.pk, 'child_set-0-name': '_invalid', 'child_set-INITIAL_FORMS': 1, 'child_set-MAX_NUM_FORMS': 1000, 'child_set-MIN_NUM_FORMS': 0, 'child_set-TOTAL_FORMS': 4, 'name': 'Father', }) self.assertContains(response, 'Please correct the error below.') self.assertFalse(response.context['show_save_and_add_another']) self.assertFalse(response.context['show_save_and_continue']) self.assertTrue(response.context['show_save_as_new']) @override_settings(ROOT_URLCONF='admin_views.urls') class CustomModelAdminTest(AdminViewBasicTestCase): def test_custom_admin_site_login_form(self): self.client.logout() response = self.client.get(reverse('admin2:index'), follow=True) self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) login = self.client.post(reverse('admin2:login'), { REDIRECT_FIELD_NAME: reverse('admin2:index'), 'username': 'customform', 'password': 'secret', }, follow=True) self.assertIsInstance(login, TemplateResponse) self.assertEqual(login.status_code, 200) self.assertContains(login, 'custom form error') self.assertContains(login, 'path/to/media.css') def test_custom_admin_site_login_template(self): self.client.logout() response = self.client.get(reverse('admin2:index'), follow=True) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/login.html') self.assertContains(response, 'Hello from a custom login template') def test_custom_admin_site_logout_template(self): response = self.client.get(reverse('admin2:logout')) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/logout.html') self.assertContains(response, 'Hello from a custom logout template') def test_custom_admin_site_index_view_and_template(self): response = self.client.get(reverse('admin2:index')) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/index.html') self.assertContains(response, 'Hello from a custom index template *bar*') def test_custom_admin_site_app_index_view_and_template(self): response = self.client.get(reverse('admin2:app_list', args=('admin_views',))) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/app_index.html') self.assertContains(response, 'Hello from a custom app_index template') def test_custom_admin_site_password_change_template(self): response = self.client.get(reverse('admin2:password_change')) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/password_change_form.html') self.assertContains(response, 'Hello from a custom password change form template') def test_custom_admin_site_password_change_with_extra_context(self): response = self.client.get(reverse('admin2:password_change')) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/password_change_form.html') self.assertContains(response, 'eggs') def test_custom_admin_site_password_change_done_template(self): response = self.client.get(reverse('admin2:password_change_done')) self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/password_change_done.html') self.assertContains(response, 'Hello from a custom password change done template') def test_custom_admin_site_view(self): self.client.force_login(self.superuser) response = self.client.get(reverse('admin2:my_view')) self.assertEqual(response.content, b"Django is a magical pony!") def test_pwd_change_custom_template(self): self.client.force_login(self.superuser) su = User.objects.get(username='super') response = self.client.get(reverse('admin4:auth_user_password_change', args=(su.pk,))) self.assertEqual(response.status_code, 200) def get_perm(Model, perm): """Return the permission object, for the Model""" ct = ContentType.objects.get_for_model(Model) return Permission.objects.get(content_type=ct, codename=perm) @override_settings( ROOT_URLCONF='admin_views.urls', # Test with the admin's documented list of required context processors. TEMPLATES=[{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }], ) class AdminViewPermissionsTest(TestCase): """Tests for Admin Views Permissions.""" @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.adduser = User.objects.create_user(username='adduser', password='secret', is_staff=True) cls.changeuser = User.objects.create_user(username='changeuser', password='secret', is_staff=True) cls.deleteuser = User.objects.create_user(username='deleteuser', password='secret', is_staff=True) cls.joepublicuser = User.objects.create_user(username='joepublic', password='secret') cls.nostaffuser = User.objects.create_user(username='nostaff', password='secret') cls.s1 = Section.objects.create(name='Test section') cls.a1 = Article.objects.create( content='

Middle content

', date=datetime.datetime(2008, 3, 18, 11, 54, 58), section=cls.s1, another_section=cls.s1, ) cls.a2 = Article.objects.create( content='

Oldest content

', date=datetime.datetime(2000, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a3 = Article.objects.create( content='

Newest content

', date=datetime.datetime(2009, 3, 18, 11, 54, 58), section=cls.s1 ) cls.p1 = PrePopulatedPost.objects.create(title='A Long Title', published=True, slug='a-long-title') # Setup permissions, for our users who can add, change, and delete. opts = Article._meta # User who can add Articles cls.adduser.user_permissions.add(get_perm(Article, get_permission_codename('add', opts))) # User who can change Articles cls.changeuser.user_permissions.add(get_perm(Article, get_permission_codename('change', opts))) cls.nostaffuser.user_permissions.add(get_perm(Article, get_permission_codename('change', opts))) # User who can delete Articles cls.deleteuser.user_permissions.add(get_perm(Article, get_permission_codename('delete', opts))) cls.deleteuser.user_permissions.add(get_perm(Section, get_permission_codename('delete', Section._meta))) # login POST dicts cls.index_url = reverse('admin:index') cls.super_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'super', 'password': 'secret', } cls.super_email_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'super@example.com', 'password': 'secret', } cls.super_email_bad_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'super@example.com', 'password': 'notsecret', } cls.adduser_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'adduser', 'password': 'secret', } cls.changeuser_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'changeuser', 'password': 'secret', } cls.deleteuser_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'deleteuser', 'password': 'secret', } cls.nostaff_login = { REDIRECT_FIELD_NAME: reverse('has_permission_admin:index'), 'username': 'nostaff', 'password': 'secret', } cls.joepublic_login = { REDIRECT_FIELD_NAME: cls.index_url, 'username': 'joepublic', 'password': 'secret', } cls.no_username_login = { REDIRECT_FIELD_NAME: cls.index_url, 'password': 'secret', } def test_login(self): """ Make sure only staff members can log in. Successful posts to the login page will redirect to the original url. Unsuccessful attempts will continue to render the login page with a 200 status code. """ login_url = '%s?next=%s' % (reverse('admin:login'), reverse('admin:index')) # Super User response = self.client.get(self.index_url) self.assertRedirects(response, login_url) login = self.client.post(login_url, self.super_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) self.client.get(reverse('admin:logout')) # Test if user enters email address response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.super_email_login) self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post(login_url, self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post(login_url, self.super_email_login) self.assertContains(login, ERROR_MESSAGE) # Add User response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.adduser_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) self.client.get(reverse('admin:logout')) # Change User response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.changeuser_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) self.client.get(reverse('admin:logout')) # Delete User response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.deleteuser_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) self.client.get(reverse('admin:logout')) # Regular User should not be able to login. response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.joepublic_login) self.assertEqual(login.status_code, 200) self.assertContains(login, ERROR_MESSAGE) # Requests without username should not return 500 errors. response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) login = self.client.post(login_url, self.no_username_login) self.assertEqual(login.status_code, 200) self.assertFormError(login, 'form', 'username', ['This field is required.']) def test_login_redirect_for_direct_get(self): """ Login redirect should be to the admin index page when going directly to /admin/login/. """ response = self.client.get(reverse('admin:login')) self.assertEqual(response.status_code, 200) self.assertEqual(response.context[REDIRECT_FIELD_NAME], reverse('admin:index')) def test_login_has_permission(self): # Regular User should not be able to login. response = self.client.get(reverse('has_permission_admin:index')) self.assertEqual(response.status_code, 302) login = self.client.post(reverse('has_permission_admin:login'), self.joepublic_login) self.assertEqual(login.status_code, 200) self.assertContains(login, 'permission denied') # User with permissions should be able to login. response = self.client.get(reverse('has_permission_admin:index')) self.assertEqual(response.status_code, 302) login = self.client.post(reverse('has_permission_admin:login'), self.nostaff_login) self.assertRedirects(login, reverse('has_permission_admin:index')) self.assertFalse(login.context) self.client.get(reverse('has_permission_admin:logout')) # Staff should be able to login. response = self.client.get(reverse('has_permission_admin:index')) self.assertEqual(response.status_code, 302) login = self.client.post(reverse('has_permission_admin:login'), { REDIRECT_FIELD_NAME: reverse('has_permission_admin:index'), 'username': 'deleteuser', 'password': 'secret', }) self.assertRedirects(login, reverse('has_permission_admin:index')) self.assertFalse(login.context) self.client.get(reverse('has_permission_admin:logout')) def test_login_successfully_redirects_to_original_URL(self): response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) query_string = 'the-answer=42' redirect_url = '%s?%s' % (self.index_url, query_string) new_next = {REDIRECT_FIELD_NAME: redirect_url} post_data = self.super_login.copy() post_data.pop(REDIRECT_FIELD_NAME) login = self.client.post( '%s?%s' % (reverse('admin:login'), urlencode(new_next)), post_data) self.assertRedirects(login, redirect_url) def test_double_login_is_not_allowed(self): """Regression test for #19327""" login_url = '%s?next=%s' % (reverse('admin:login'), reverse('admin:index')) response = self.client.get(self.index_url) self.assertEqual(response.status_code, 302) # Establish a valid admin session login = self.client.post(login_url, self.super_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) # Logging in with non-admin user fails login = self.client.post(login_url, self.joepublic_login) self.assertEqual(login.status_code, 200) self.assertContains(login, ERROR_MESSAGE) # Establish a valid admin session login = self.client.post(login_url, self.super_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) # Logging in with admin user while already logged in login = self.client.post(login_url, self.super_login) self.assertRedirects(login, self.index_url) self.assertFalse(login.context) self.client.get(reverse('admin:logout')) def test_login_page_notice_for_non_staff_users(self): """ A logged-in non-staff user trying to access the admin index should be presented with the login page and a hint indicating that the current user doesn't have access to it. """ hint_template = 'You are authenticated as {}' # Anonymous user should not be shown the hint response = self.client.get(self.index_url, follow=True) self.assertContains(response, 'login-form') self.assertNotContains(response, hint_template.format(''), status_code=200) # Non-staff user should be shown the hint self.client.force_login(self.nostaffuser) response = self.client.get(self.index_url, follow=True) self.assertContains(response, 'login-form') self.assertContains(response, hint_template.format(self.nostaffuser.username), status_code=200) def test_add_view(self): """Test add view restricts access and actually adds items.""" add_dict = {'title': 'Døm ikke', 'content': '

great article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': self.s1.pk} # Change User should not have access to add articles self.client.force_login(self.changeuser) # make sure the view removes test cookie self.assertIs(self.client.session.test_cookie_worked(), False) response = self.client.get(reverse('admin:admin_views_article_add')) self.assertEqual(response.status_code, 403) # Try POST just to make sure post = self.client.post(reverse('admin:admin_views_article_add'), add_dict) self.assertEqual(post.status_code, 403) self.assertEqual(Article.objects.count(), 3) self.client.get(reverse('admin:logout')) # Add user may login and POST to add view, then redirect to admin root self.client.force_login(self.adduser) addpage = self.client.get(reverse('admin:admin_views_article_add')) change_list_link = '› Articles' % reverse('admin:admin_views_article_changelist') self.assertNotContains( addpage, change_list_link, msg_prefix='User restricted to add permission is given link to change list view in breadcrumbs.' ) post = self.client.post(reverse('admin:admin_views_article_add'), add_dict) self.assertRedirects(post, self.index_url) self.assertEqual(Article.objects.count(), 4) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object') self.client.get(reverse('admin:logout')) # The addition was logged correctly addition_log = LogEntry.objects.all()[0] new_article = Article.objects.last() article_ct = ContentType.objects.get_for_model(Article) self.assertEqual(addition_log.user_id, self.adduser.pk) self.assertEqual(addition_log.content_type_id, article_ct.pk) self.assertEqual(addition_log.object_id, str(new_article.pk)) self.assertEqual(addition_log.object_repr, "Døm ikke") self.assertEqual(addition_log.action_flag, ADDITION) self.assertEqual(addition_log.get_change_message(), "Added.") # Super can add too, but is redirected to the change list view self.client.force_login(self.superuser) addpage = self.client.get(reverse('admin:admin_views_article_add')) self.assertContains( addpage, change_list_link, msg_prefix='Unrestricted user is not given link to change list view in breadcrumbs.' ) post = self.client.post(reverse('admin:admin_views_article_add'), add_dict) self.assertRedirects(post, reverse('admin:admin_views_article_changelist')) self.assertEqual(Article.objects.count(), 5) self.client.get(reverse('admin:logout')) # 8509 - if a normal user is already logged in, it is possible # to change user into the superuser without error self.client.force_login(self.joepublicuser) # Check and make sure that if user expires, data still persists self.client.force_login(self.superuser) # make sure the view removes test cookie self.assertIs(self.client.session.test_cookie_worked(), False) def test_change_view(self): """Change view should restrict access and allow users to edit items.""" change_dict = {'title': 'Ikke fordømt', 'content': '

edited article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': self.s1.pk} article_change_url = reverse('admin:admin_views_article_change', args=(self.a1.pk,)) article_changelist_url = reverse('admin:admin_views_article_changelist') # add user should not be able to view the list of article or change any of them self.client.force_login(self.adduser) response = self.client.get(article_changelist_url) self.assertEqual(response.status_code, 403) response = self.client.get(article_change_url) self.assertEqual(response.status_code, 403) post = self.client.post(article_change_url, change_dict) self.assertEqual(post.status_code, 403) self.client.get(reverse('admin:logout')) # change user can view all items and edit them self.client.force_login(self.changeuser) response = self.client.get(article_changelist_url) self.assertEqual(response.status_code, 200) response = self.client.get(article_change_url) self.assertEqual(response.status_code, 200) post = self.client.post(article_change_url, change_dict) self.assertRedirects(post, article_changelist_url) self.assertEqual(Article.objects.get(pk=self.a1.pk).content, '

edited article

') # one error in form should produce singular error message, multiple errors plural change_dict['title'] = '' post = self.client.post(article_change_url, change_dict) self.assertContains( post, 'Please correct the error below.', msg_prefix='Singular error message not found in response to post with one error' ) change_dict['content'] = '' post = self.client.post(article_change_url, change_dict) self.assertContains( post, 'Please correct the errors below.', msg_prefix='Plural error message not found in response to post with multiple errors' ) self.client.get(reverse('admin:logout')) # Test redirection when using row-level change permissions. Refs #11513. r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id") r2 = RowLevelChangePermissionModel.objects.create(id=2, name="even id") change_url_1 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r1.pk,)) change_url_2 = reverse('admin:admin_views_rowlevelchangepermissionmodel_change', args=(r2.pk,)) for login_user in [self.superuser, self.adduser, self.changeuser, self.deleteuser]: self.client.force_login(login_user) response = self.client.get(change_url_1) self.assertEqual(response.status_code, 403) response = self.client.post(change_url_1, {'name': 'changed'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') self.assertEqual(response.status_code, 403) response = self.client.get(change_url_2) self.assertEqual(response.status_code, 200) response = self.client.post(change_url_2, {'name': 'changed'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') self.assertRedirects(response, self.index_url) self.client.get(reverse('admin:logout')) for login_user in [self.joepublicuser, self.nostaffuser]: self.client.force_login(login_user) response = self.client.get(change_url_1, follow=True) self.assertContains(response, 'login-form') response = self.client.post(change_url_1, {'name': 'changed'}, follow=True) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') self.assertContains(response, 'login-form') response = self.client.get(change_url_2, follow=True) self.assertContains(response, 'login-form') response = self.client.post(change_url_2, {'name': 'changed again'}, follow=True) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') self.assertContains(response, 'login-form') self.client.get(reverse('admin:logout')) def test_change_view_save_as_new(self): """ 'Save as new' should raise PermissionDenied for users without the 'add' permission. """ change_dict_save_as_new = { '_saveasnew': 'Save as new', 'title': 'Ikke fordømt', 'content': '

edited article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': self.s1.pk, } article_change_url = reverse('admin:admin_views_article_change', args=(self.a1.pk,)) # Add user can perform "Save as new". article_count = Article.objects.count() self.client.force_login(self.adduser) post = self.client.post(article_change_url, change_dict_save_as_new) self.assertRedirects(post, self.index_url) self.assertEqual(Article.objects.count(), article_count + 1) self.client.logout() # Change user cannot perform "Save as new" (no 'add' permission). article_count = Article.objects.count() self.client.force_login(self.changeuser) post = self.client.post(article_change_url, change_dict_save_as_new) self.assertEqual(post.status_code, 403) self.assertEqual(Article.objects.count(), article_count) # User with both add and change permissions should be redirected to the # change page for the newly created object. article_count = Article.objects.count() self.client.force_login(self.superuser) post = self.client.post(article_change_url, change_dict_save_as_new) self.assertEqual(Article.objects.count(), article_count + 1) new_article = Article.objects.latest('id') self.assertRedirects(post, reverse('admin:admin_views_article_change', args=(new_article.pk,))) def test_delete_view(self): """Delete view should restrict access and actually delete items.""" delete_dict = {'post': 'yes'} delete_url = reverse('admin:admin_views_article_delete', args=(self.a1.pk,)) # add user should not be able to delete articles self.client.force_login(self.adduser) response = self.client.get(delete_url) self.assertEqual(response.status_code, 403) post = self.client.post(delete_url, delete_dict) self.assertEqual(post.status_code, 403) self.assertEqual(Article.objects.count(), 3) self.client.logout() # Delete user can delete self.client.force_login(self.deleteuser) response = self.client.get(reverse('admin:admin_views_section_delete', args=(self.s1.pk,))) self.assertContains(response, "

Summary

") self.assertContains(response, "
  • Articles: 3
  • ") # test response contains link to related Article self.assertContains(response, "admin_views/article/%s/" % self.a1.pk) response = self.client.get(delete_url) self.assertContains(response, "admin_views/article/%s/" % self.a1.pk) self.assertContains(response, "

    Summary

    ") self.assertContains(response, "
  • Articles: 1
  • ") self.assertEqual(response.status_code, 200) post = self.client.post(delete_url, delete_dict) self.assertRedirects(post, self.index_url) self.assertEqual(Article.objects.count(), 2) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, 'Greetings from a deleted object') article_ct = ContentType.objects.get_for_model(Article) logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION) self.assertEqual(logged.object_id, str(self.a1.pk)) def test_delete_view_nonexistent_obj(self): self.client.force_login(self.deleteuser) url = reverse('admin:admin_views_article_delete', args=('nonexistent',)) response = self.client.get(url, follow=True) self.assertRedirects(response, reverse('admin:index')) self.assertEqual( [m.message for m in response.context['messages']], ["""article with ID "nonexistent" doesn't exist. Perhaps it was deleted?"""] ) def test_history_view(self): """History view should restrict access.""" # add user should not be able to view the list of article or change any of them self.client.force_login(self.adduser) response = self.client.get(reverse('admin:admin_views_article_history', args=(self.a1.pk,))) self.assertEqual(response.status_code, 403) self.client.get(reverse('admin:logout')) # change user can view all items and edit them self.client.force_login(self.changeuser) response = self.client.get(reverse('admin:admin_views_article_history', args=(self.a1.pk,))) self.assertEqual(response.status_code, 200) # Test redirection when using row-level change permissions. Refs #11513. rl1 = RowLevelChangePermissionModel.objects.create(name="odd id") rl2 = RowLevelChangePermissionModel.objects.create(name="even id") for login_user in [self.superuser, self.adduser, self.changeuser, self.deleteuser]: self.client.force_login(login_user) url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl1.pk,)) response = self.client.get(url) self.assertEqual(response.status_code, 403) url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl2.pk,)) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.client.get(reverse('admin:logout')) for login_user in [self.joepublicuser, self.nostaffuser]: self.client.force_login(login_user) url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl1.pk,)) response = self.client.get(url, follow=True) self.assertContains(response, 'login-form') url = reverse('admin:admin_views_rowlevelchangepermissionmodel_history', args=(rl2.pk,)) response = self.client.get(url, follow=True) self.assertContains(response, 'login-form') self.client.get(reverse('admin:logout')) def test_history_view_bad_url(self): self.client.force_login(self.changeuser) response = self.client.get(reverse('admin:admin_views_article_history', args=('foo',)), follow=True) self.assertRedirects(response, reverse('admin:index')) self.assertEqual( [m.message for m in response.context['messages']], ["""article with ID "foo" doesn't exist. Perhaps it was deleted?"""] ) def test_conditionally_show_add_section_link(self): """ The foreign key widget should only show the "add related" button if the user has permission to add that related item. """ self.client.force_login(self.adduser) # The user can't add sections yet, so they shouldn't see the "add section" link. url = reverse('admin:admin_views_article_add') add_link_text = 'add_id_section' response = self.client.get(url) self.assertNotContains(response, add_link_text) # Allow the user to add sections too. Now they can see the "add section" link. user = User.objects.get(username='adduser') perm = get_perm(Section, get_permission_codename('add', Section._meta)) user.user_permissions.add(perm) response = self.client.get(url) self.assertContains(response, add_link_text) def test_conditionally_show_change_section_link(self): """ The foreign key widget should only show the "change related" button if the user has permission to change that related item. """ def get_change_related(response): return response.context['adminform'].form.fields['section'].widget.can_change_related self.client.force_login(self.adduser) # The user can't change sections yet, so they shouldn't see the "change section" link. url = reverse('admin:admin_views_article_add') change_link_text = 'change_id_section' response = self.client.get(url) self.assertFalse(get_change_related(response)) self.assertNotContains(response, change_link_text) # Allow the user to change sections too. Now they can see the "change section" link. user = User.objects.get(username='adduser') perm = get_perm(Section, get_permission_codename('change', Section._meta)) user.user_permissions.add(perm) response = self.client.get(url) self.assertTrue(get_change_related(response)) self.assertContains(response, change_link_text) def test_conditionally_show_delete_section_link(self): """ The foreign key widget should only show the "delete related" button if the user has permission to delete that related item. """ def get_delete_related(response): return response.context['adminform'].form.fields['sub_section'].widget.can_delete_related self.client.force_login(self.adduser) # The user can't delete sections yet, so they shouldn't see the "delete section" link. url = reverse('admin:admin_views_article_add') delete_link_text = 'delete_id_sub_section' response = self.client.get(url) self.assertFalse(get_delete_related(response)) self.assertNotContains(response, delete_link_text) # Allow the user to delete sections too. Now they can see the "delete section" link. user = User.objects.get(username='adduser') perm = get_perm(Section, get_permission_codename('delete', Section._meta)) user.user_permissions.add(perm) response = self.client.get(url) self.assertTrue(get_delete_related(response)) self.assertContains(response, delete_link_text) def test_disabled_permissions_when_logged_in(self): self.client.force_login(self.superuser) superuser = User.objects.get(username='super') superuser.is_active = False superuser.save() response = self.client.get(self.index_url, follow=True) self.assertContains(response, 'id="login-form"') self.assertNotContains(response, 'Log out') response = self.client.get(reverse('secure_view'), follow=True) self.assertContains(response, 'id="login-form"') def test_disabled_staff_permissions_when_logged_in(self): self.client.force_login(self.superuser) superuser = User.objects.get(username='super') superuser.is_staff = False superuser.save() response = self.client.get(self.index_url, follow=True) self.assertContains(response, 'id="login-form"') self.assertNotContains(response, 'Log out') response = self.client.get(reverse('secure_view'), follow=True) self.assertContains(response, 'id="login-form"') def test_app_list_permissions(self): """ If a user has no module perms, the app list returns a 404. """ opts = Article._meta change_user = User.objects.get(username='changeuser') permission = get_perm(Article, get_permission_codename('change', opts)) self.client.force_login(self.changeuser) # the user has no module permissions change_user.user_permissions.remove(permission) response = self.client.get(reverse('admin:app_list', args=('admin_views',))) self.assertEqual(response.status_code, 404) # the user now has module permissions change_user.user_permissions.add(permission) response = self.client.get(reverse('admin:app_list', args=('admin_views',))) self.assertEqual(response.status_code, 200) def test_shortcut_view_only_available_to_staff(self): """ Only admin users should be able to use the admin shortcut view. """ model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey) obj = ModelWithStringPrimaryKey.objects.create(string_pk='foo') shortcut_url = reverse('admin:view_on_site', args=(model_ctype.pk, obj.pk)) # Not logged in: we should see the login page. response = self.client.get(shortcut_url, follow=True) self.assertTemplateUsed(response, 'admin/login.html') # Logged in? Redirect. self.client.force_login(self.superuser) response = self.client.get(shortcut_url, follow=False) # Can't use self.assertRedirects() because User.get_absolute_url() is silly. self.assertEqual(response.status_code, 302) # Domain may depend on contrib.sites tests also run self.assertRegex(response.url, 'http://(testserver|example.com)/dummy/foo/') def test_has_module_permission(self): """ has_module_permission() returns True for all users who have any permission for that module (add, change, or delete), so that the module is displayed on the admin index page. """ self.client.force_login(self.superuser) response = self.client.get(self.index_url) self.assertContains(response, 'admin_views') self.assertContains(response, 'Articles') self.client.logout() self.client.force_login(self.adduser) response = self.client.get(self.index_url) self.assertContains(response, 'admin_views') self.assertContains(response, 'Articles') self.client.logout() self.client.force_login(self.changeuser) response = self.client.get(self.index_url) self.assertContains(response, 'admin_views') self.assertContains(response, 'Articles') self.client.logout() self.client.force_login(self.deleteuser) response = self.client.get(self.index_url) self.assertContains(response, 'admin_views') self.assertContains(response, 'Articles') def test_overriding_has_module_permission(self): """ If has_module_permission() always returns False, the module shouldn't be displayed on the admin index page for any users. """ articles = Article._meta.verbose_name_plural.title() sections = Section._meta.verbose_name_plural.title() index_url = reverse('admin7:index') self.client.force_login(self.superuser) response = self.client.get(index_url) self.assertContains(response, sections) self.assertNotContains(response, articles) self.client.logout() self.client.force_login(self.adduser) response = self.client.get(index_url) self.assertNotContains(response, 'admin_views') self.assertNotContains(response, articles) self.client.logout() self.client.force_login(self.changeuser) response = self.client.get(index_url) self.assertNotContains(response, 'admin_views') self.assertNotContains(response, articles) self.client.logout() self.client.force_login(self.deleteuser) response = self.client.get(index_url) self.assertNotContains(response, articles) # The app list displays Sections but not Articles as the latter has # ModelAdmin.has_module_permission() = False. self.client.force_login(self.superuser) response = self.client.get(reverse('admin7:app_list', args=('admin_views',))) self.assertContains(response, sections) self.assertNotContains(response, articles) def test_post_save_message_no_forbidden_links_visible(self): """ Post-save message shouldn't contain a link to the change form if the user doen't have the change permission. """ self.client.force_login(self.adduser) # Emulate Article creation for user with add-only permission. post_data = { "title": "Fun & games", "content": "Some content", "date_0": "2015-10-31", "date_1": "16:35:00", "_save": "Save", } response = self.client.post(reverse('admin:admin_views_article_add'), post_data, follow=True) self.assertContains( response, '
  • The article "Fun & games" was added successfully.
  • ', html=True ) @override_settings(ROOT_URLCONF='admin_views.urls') class AdminViewsNoUrlTest(TestCase): """Regression test for #17333""" @classmethod def setUpTestData(cls): # User who can change Reports cls.changeuser = User.objects.create_user(username='changeuser', password='secret', is_staff=True) cls.changeuser.user_permissions.add(get_perm(Report, get_permission_codename('change', Report._meta))) def test_no_standard_modeladmin_urls(self): """Admin index views don't break when user's ModelAdmin removes standard urls""" self.client.force_login(self.changeuser) r = self.client.get(reverse('admin:index')) # we shouldn't get a 500 error caused by a NoReverseMatch self.assertEqual(r.status_code, 200) self.client.get(reverse('admin:logout')) @skipUnlessDBFeature('can_defer_constraint_checks') @override_settings(ROOT_URLCONF='admin_views.urls') class AdminViewDeletedObjectsTest(TestCase): @classmethod def setUpTestData(cls): cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') cls.deleteuser = User.objects.create_user(username='deleteuser', password='secret', is_staff=True) cls.s1 = Section.objects.create(name='Test section') cls.a1 = Article.objects.create( content='

    Middle content

    ', date=datetime.datetime(2008, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a2 = Article.objects.create( content='

    Oldest content

    ', date=datetime.datetime(2000, 3, 18, 11, 54, 58), section=cls.s1 ) cls.a3 = Article.objects.create( content='

    Newest content

    ', date=datetime.datetime(2009, 3, 18, 11, 54, 58), section=cls.s1 ) cls.p1 = PrePopulatedPost.objects.create(title='A Long Title', published=True, slug='a-long-title') cls.v1 = Villain.objects.create(name='Adam') cls.v2 = Villain.objects.create(name='Sue') cls.sv1 = SuperVillain.objects.create(name='Bob') cls.pl1 = Plot.objects.create(name='World Domination', team_leader=cls.v1, contact=cls.v2) cls.pl2 = Plot.objects.create(name='World Peace', team_leader=cls.v2, contact=cls.v2) cls.pl3 = Plot.objects.create(name='Corn Conspiracy', team_leader=cls.v1, contact=cls.v1) cls.pd1 = PlotDetails.objects.create(details='almost finished', plot=cls.pl1) cls.sh1 = SecretHideout.objects.create(location='underground bunker', villain=cls.v1) cls.sh2 = SecretHideout.objects.create(location='floating castle', villain=cls.sv1) cls.ssh1 = SuperSecretHideout.objects.create(location='super floating castle!', supervillain=cls.sv1) cls.cy1 = CyclicOne.objects.create(name='I am recursive', two_id=1) cls.cy2 = CyclicTwo.objects.create(name='I am recursive too', one_id=1) def setUp(self): self.client.force_login(self.superuser) def test_nesting(self): """ Objects should be nested to display the relationships that cause them to be scheduled for deletion. """ pattern = re.compile( force_bytes( r'
  • Plot: World Domination\s*