django.contrib.admin admin_order_field

Description

  • Added support for the admin_order_field attribute on properties in ModelAdmin.list_display.

django/contrib/admin/views/main.py

  1from datetime import datetime, timedelta
  2
  3from django import forms
  4from django.conf import settings
  5from django.contrib import messages
  6from django.contrib.admin import FieldListFilter
  7from django.contrib.admin.exceptions import (
  8    DisallowedModelAdminLookup,
  9    DisallowedModelAdminToField,
 10)
 11from django.contrib.admin.options import (
 12    IS_POPUP_VAR,
 13    TO_FIELD_VAR,
 14    IncorrectLookupParameters,
 15)
 16from django.contrib.admin.utils import (
 17    get_fields_from_path,
 18    lookup_needs_distinct,
 19    prepare_lookup_value,
 20    quote,
 21)
 22from django.core.exceptions import (
 23    FieldDoesNotExist,
 24    ImproperlyConfigured,
 25    SuspiciousOperation,
 26)
 27from django.core.paginator import InvalidPage
 28from django.db import models
 29from django.db.models.expressions import Combinable, F, OrderBy
 30from django.urls import reverse
 31from django.utils.http import urlencode
 32from django.utils.timezone import make_aware
 33from django.utils.translation import gettext
 34
 35# Changelist settings
 36ALL_VAR = "all"
 37ORDER_VAR = "o"
 38ORDER_TYPE_VAR = "ot"
 39PAGE_VAR = "p"
 40SEARCH_VAR = "q"
 41ERROR_FLAG = "e"
 42
 43IGNORED_PARAMS = (
 44    ALL_VAR,
 45    ORDER_VAR,
 46    ORDER_TYPE_VAR,
 47    SEARCH_VAR,
 48    IS_POPUP_VAR,
 49    TO_FIELD_VAR,
 50)
 51
 52
 53class ChangeListSearchForm(forms.Form):
 54    def __init__(self, *args, **kwargs):
 55        super().__init__(*args, **kwargs)
 56        # Populate "fields" dynamically because SEARCH_VAR is a variable:
 57        self.fields = {
 58            SEARCH_VAR: forms.CharField(required=False, strip=False),
 59        }
 60
 61
 62class ChangeList:
 63    search_form_class = ChangeListSearchForm
 64
 65    def __init__(
 66        self,
 67        request,
 68        model,
 69        list_display,
 70        list_display_links,
 71        list_filter,
 72        date_hierarchy,
 73        search_fields,
 74        list_select_related,
 75        list_per_page,
 76        list_max_show_all,
 77        list_editable,
 78        model_admin,
 79        sortable_by,
 80    ):
 81        self.model = model
 82        self.opts = model._meta
 83        self.lookup_opts = self.opts
 84        self.root_queryset = model_admin.get_queryset(request)
 85        self.list_display = list_display
 86        self.list_display_links = list_display_links
 87        self.list_filter = list_filter
 88        self.has_filters = None
 89        self.date_hierarchy = date_hierarchy
 90        self.search_fields = search_fields
 91        self.list_select_related = list_select_related
 92        self.list_per_page = list_per_page
 93        self.list_max_show_all = list_max_show_all
 94        self.model_admin = model_admin
 95        self.preserved_filters = model_admin.get_preserved_filters(request)
 96        self.sortable_by = sortable_by
 97
 98        # Get search parameters from the query string.
 99        _search_form = self.search_form_class(request.GET)
100        if not _search_form.is_valid():
101            for error in _search_form.errors.values():
102                messages.error(request, ", ".join(error))
103        self.query = _search_form.cleaned_data.get(SEARCH_VAR) or ""
104        try:
105            self.page_num = int(request.GET.get(PAGE_VAR, 0))
106        except ValueError:
107            self.page_num = 0
108        self.show_all = ALL_VAR in request.GET
109        self.is_popup = IS_POPUP_VAR in request.GET
110        to_field = request.GET.get(TO_FIELD_VAR)
111        if to_field and not model_admin.to_field_allowed(request, to_field):
112            raise DisallowedModelAdminToField(
113                "The field %s cannot be referenced." % to_field
114            )
115        self.to_field = to_field
116        self.params = dict(request.GET.items())
117        if PAGE_VAR in self.params:
118            del self.params[PAGE_VAR]
119        if ERROR_FLAG in self.params:
120            del self.params[ERROR_FLAG]
121
122        if self.is_popup:
123            self.list_editable = ()
124        else:
125            self.list_editable = list_editable
126        self.queryset = self.get_queryset(request)
127        self.get_results(request)
128        if self.is_popup:
129            title = gettext("Select %s")
130        elif self.model_admin.has_change_permission(request):
131            title = gettext("Select %s to change")
132        else:
133            title = gettext("Select %s to view")
134        self.title = title % self.opts.verbose_name
135        self.pk_attname = self.lookup_opts.pk.attname
136
137    def get_filters_params(self, params=None):
138        """
139        Return all params except IGNORED_PARAMS.
140        """
141        params = params or self.params
142        lookup_params = params.copy()  # a dictionary of the query string
143        # Remove all the parameters that are globally and systematically
144        # ignored.
145        for ignored in IGNORED_PARAMS:
146            if ignored in lookup_params:
147                del lookup_params[ignored]
148        return lookup_params
149
150    def get_filters(self, request):
151        lookup_params = self.get_filters_params()
152        use_distinct = False
153
154        for key, value in lookup_params.items():
155            if not self.model_admin.lookup_allowed(key, value):
156                raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key)
157
158        filter_specs = []
159        for list_filter in self.list_filter:
160            if callable(list_filter):
161                # This is simply a custom list filter class.
162                spec = list_filter(request, lookup_params, self.model, self.model_admin)
163            else:
164                field_path = None
165                if isinstance(list_filter, (tuple, list)):
166                    # This is a custom FieldListFilter class for a given field.
167                    field, field_list_filter_class = list_filter
168                else:
169                    # This is simply a field name, so use the default
170                    # FieldListFilter class that has been registered for the
171                    # type of the given field.
172                    field, field_list_filter_class = list_filter, FieldListFilter.create
173                if not isinstance(field, models.Field):
174                    field_path = field
175                    field = get_fields_from_path(self.model, field_path)[-1]
176
177                lookup_params_count = len(lookup_params)
178                spec = field_list_filter_class(
179                    field,
180                    request,
181                    lookup_params,
182                    self.model,
183                    self.model_admin,
184                    field_path=field_path,
185                )
186                # field_list_filter_class removes any lookup_params it
187                # processes. If that happened, check if distinct() is needed to
188                # remove duplicate results.
189                if lookup_params_count > len(lookup_params):
190                    use_distinct = use_distinct or lookup_needs_distinct(
191                        self.lookup_opts, field_path
192                    )
193            if spec and spec.has_output():
194                filter_specs.append(spec)
195
196        if self.date_hierarchy:
197            # Create bounded lookup parameters so that the query is more
198            # efficient.
199            year = lookup_params.pop("%s__year" % self.date_hierarchy, None)
200            if year is not None:
201                month = lookup_params.pop("%s__month" % self.date_hierarchy, None)
202                day = lookup_params.pop("%s__day" % self.date_hierarchy, None)
203                try:
204                    from_date = datetime(
205                        int(year),
206                        int(month if month is not None else 1),
207                        int(day if day is not None else 1),
208                    )
209                except ValueError as e:
210                    raise IncorrectLookupParameters(e) from e
211                if day:
212                    to_date = from_date + timedelta(days=1)
213                elif month:
214                    # In this branch, from_date will always be the first of a
215                    # month, so advancing 32 days gives the next month.
216                    to_date = (from_date + timedelta(days=32)).replace(day=1)
217                else:
218                    to_date = from_date.replace(year=from_date.year + 1)
219                if settings.USE_TZ:
220                    from_date = make_aware(from_date)
221                    to_date = make_aware(to_date)
222                lookup_params.update(
223                    {
224                        "%s__gte" % self.date_hierarchy: from_date,
225                        "%s__lt" % self.date_hierarchy: to_date,
226                    }
227                )
228
229        # At this point, all the parameters used by the various ListFilters
230        # have been removed from lookup_params, which now only contains other
231        # parameters passed via the query string. We now loop through the
232        # remaining parameters both to ensure that all the parameters are valid
233        # fields and to determine if at least one of them needs distinct(). If
234        # the lookup parameters aren't real fields, then bail out.
235        try:
236            for key, value in lookup_params.items():
237                lookup_params[key] = prepare_lookup_value(key, value)
238                use_distinct = use_distinct or lookup_needs_distinct(
239                    self.lookup_opts, key
240                )
241            return filter_specs, bool(filter_specs), lookup_params, use_distinct
242        except FieldDoesNotExist as e:
243            raise IncorrectLookupParameters(e) from e
244
245    def get_query_string(self, new_params=None, remove=None):
246        if new_params is None:
247            new_params = {}
248        if remove is None:
249            remove = []
250        p = self.params.copy()
251        for r in remove:
252            for k in list(p):
253                if k.startswith(r):
254                    del p[k]
255        for k, v in new_params.items():
256            if v is None:
257                if k in p:
258                    del p[k]
259            else:
260                p[k] = v
261        return "?%s" % urlencode(sorted(p.items()))
262
263    def get_results(self, request):
264        paginator = self.model_admin.get_paginator(
265            request, self.queryset, self.list_per_page
266        )
267        # Get the number of objects, with admin filters applied.
268        result_count = paginator.count
269
270        # Get the total number of objects, with no admin filters applied.
271        if self.model_admin.show_full_result_count:
272            full_result_count = self.root_queryset.count()
273        else:
274            full_result_count = None
275        can_show_all = result_count <= self.list_max_show_all
276        multi_page = result_count > self.list_per_page
277
278        # Get the list of objects to display on this page.
279        if (self.show_all and can_show_all) or not multi_page:
280            result_list = self.queryset._clone()
281        else:
282            try:
283                result_list = paginator.page(self.page_num + 1).object_list
284            except InvalidPage:
285                raise IncorrectLookupParameters
286
287        self.result_count = result_count
288        self.show_full_result_count = self.model_admin.show_full_result_count
289        # Admin actions are shown if there is at least one entry
290        # or if entries are not counted because show_full_result_count is disabled
291        self.show_admin_actions = not self.show_full_result_count or bool(
292            full_result_count
293        )
294        self.full_result_count = full_result_count
295        self.result_list = result_list
296        self.can_show_all = can_show_all
297        self.multi_page = multi_page
298        self.paginator = paginator
299
300    def _get_default_ordering(self):
301        ordering = []
302        if self.model_admin.ordering:
303            ordering = self.model_admin.ordering
304        elif self.lookup_opts.ordering:
305            ordering = self.lookup_opts.ordering
306        return ordering
307
308    def get_ordering_field(self, field_name):
309        """
310        Return the proper model field name corresponding to the given
311        field_name to use for ordering. field_name may either be the name of a
312        proper model field or the name of a method (on the admin or model) or a
313        callable with the 'admin_order_field' attribute. Return None if no
314        proper model field name can be matched.
315        """
316        try:
317            field = self.lookup_opts.get_field(field_name)
318            return field.name
319        except FieldDoesNotExist:
320            # See whether field_name is a name of a non-field
321            # that allows sorting.
322            if callable(field_name):
323                attr = field_name
324            elif hasattr(self.model_admin, field_name):
325                attr = getattr(self.model_admin, field_name)
326            else:
327                attr = getattr(self.model, field_name)
328            if isinstance(attr, property) and hasattr(attr, "fget"):
329                attr = attr.fget
330            return getattr(attr, "admin_order_field", None)
331
332    def get_ordering(self, request, queryset):
333        """
334        Return the list of ordering fields for the change list.
335        First check the get_ordering() method in model admin, then check
336        the object's default ordering. Then, any manually-specified ordering
337        from the query string overrides anything. Finally, a deterministic
338        order is guaranteed by calling _get_deterministic_ordering() with the
339        constructed ordering.
340        """
341        params = self.params
342        ordering = list(
343            self.model_admin.get_ordering(request) or self._get_default_ordering()
344        )
345        if ORDER_VAR in params:
346            # Clear ordering and used params
347            ordering = []
348            order_params = params[ORDER_VAR].split(".")
349            for p in order_params:
350                try:
351                    none, pfx, idx = p.rpartition("-")
352                    field_name = self.list_display[int(idx)]
353                    order_field = self.get_ordering_field(field_name)
354                    if not order_field:
355                        continue  # No 'admin_order_field', skip it
356                    if isinstance(order_field, OrderBy):
357                        if pfx == "-":
358                            order_field = order_field.copy()
359                            order_field.reverse_ordering()
360                        ordering.append(order_field)
361                    elif hasattr(order_field, "resolve_expression"):
362                        # order_field is an expression.
363                        ordering.append(
364                            order_field.desc() if pfx == "-" else order_field.asc()
365                        )
366                    # reverse order if order_field has already "-" as prefix
367                    elif order_field.startswith("-") and pfx == "-":
368                        ordering.append(order_field[1:])
369                    else:
370                        ordering.append(pfx + order_field)
371                except (IndexError, ValueError):
372                    continue  # Invalid ordering specified, skip it.
373
374        # Add the given query's ordering fields, if any.
375        ordering.extend(queryset.query.order_by)
376
377        return self._get_deterministic_ordering(ordering)
378
379    def _get_deterministic_ordering(self, ordering):
380        """
381        Ensure a deterministic order across all database backends. Search for a
382        single field or unique together set of fields providing a total
383        ordering. If these are missing, augment the ordering with a descendant
384        primary key.
385        """
386        ordering = list(ordering)
387        ordering_fields = set()
388        total_ordering_fields = {"pk"} | {
389            field.attname
390            for field in self.lookup_opts.fields
391            if field.unique and not field.null
392        }
393        for part in ordering:
394            # Search for single field providing a total ordering.
395            field_name = None
396            if isinstance(part, str):
397                field_name = part.lstrip("-")
398            elif isinstance(part, F):
399                field_name = part.name
400            elif isinstance(part, OrderBy) and isinstance(part.expression, F):
401                field_name = part.expression.name
402            if field_name:
403                # Normalize attname references by using get_field().
404                try:
405                    field = self.lookup_opts.get_field(field_name)
406                except FieldDoesNotExist:
407                    # Could be "?" for random ordering or a related field
408                    # lookup. Skip this part of introspection for now.
409                    continue
410                # Ordering by a related field name orders by the referenced
411                # model's ordering. Skip this part of introspection for now.
412                if field.remote_field and field_name == field.name:
413                    continue
414                if field.attname in total_ordering_fields:
415                    break
416                ordering_fields.add(field.attname)
417        else:
418            # No single total ordering field, try unique_together.
419            for field_names in self.lookup_opts.unique_together:
420                # Normalize attname references by using get_field().
421                fields = [
422                    self.lookup_opts.get_field(field_name) for field_name in field_names
423                ]
424                # Composite unique constraints containing a nullable column
425                # cannot ensure total ordering.
426                if any(field.null for field in fields):
427                    continue
428                if ordering_fields.issuperset(field.attname for field in fields):
429                    break
430            else:
431                # If no set of unique fields is present in the ordering, rely
432                # on the primary key to provide total ordering.
433                ordering.append("-pk")
434        return ordering
435
436    def get_ordering_field_columns(self):
437        """
438        Return a dictionary of ordering field column numbers and asc/desc.
439        """
440        # We must cope with more than one column having the same underlying sort
441        # field, so we base things on column numbers.
442        ordering = self._get_default_ordering()
443        ordering_fields = {}
444        if ORDER_VAR not in self.params:
445            # for ordering specified on ModelAdmin or model Meta, we don't know
446            # the right column numbers absolutely, because there might be more
447            # than one column associated with that ordering, so we guess.
448            for field in ordering:
449                if isinstance(field, (Combinable, OrderBy)):
450                    if not isinstance(field, OrderBy):
451                        field = field.asc()
452                    if isinstance(field.expression, F):
453                        order_type = "desc" if field.descending else "asc"
454                        field = field.expression.name
455                    else:
456                        continue
457                elif field.startswith("-"):
458                    field = field[1:]
459                    order_type = "desc"
460                else:
461                    order_type = "asc"
462                for index, attr in enumerate(self.list_display):
463                    if self.get_ordering_field(attr) == field:
464                        ordering_fields[index] = order_type
465                        break
466        else:
467            for p in self.params[ORDER_VAR].split("."):
468                none, pfx, idx = p.rpartition("-")
469                try:
470                    idx = int(idx)
471                except ValueError:
472                    continue  # skip it
473                ordering_fields[idx] = "desc" if pfx == "-" else "asc"
474        return ordering_fields
475
476    def get_queryset(self, request):
477        # First, we collect all the declared list filters.
478        (
479            self.filter_specs,
480            self.has_filters,
481            remaining_lookup_params,
482            filters_use_distinct,
483        ) = self.get_filters(request)
484
485        # Then, we let every list filter modify the queryset to its liking.
486        qs = self.root_queryset
487        for filter_spec in self.filter_specs:
488            new_qs = filter_spec.queryset(request, qs)
489            if new_qs is not None:
490                qs = new_qs
491
492        try:
493            # Finally, we apply the remaining lookup parameters from the query
494            # string (i.e. those that haven't already been processed by the
495            # filters).
496            qs = qs.filter(**remaining_lookup_params)
497        except (SuspiciousOperation, ImproperlyConfigured):
498            # Allow certain types of errors to be re-raised as-is so that the
499            # caller can treat them in a special way.
500            raise
501        except Exception as e:
502            # Every other error is caught with a naked except, because we don't
503            # have any other way of validating lookup parameters. They might be
504            # invalid if the keyword arguments are incorrect, or if the values
505            # are not in the correct type, so we might get FieldError,
506            # ValueError, ValidationError, or ?.
507            raise IncorrectLookupParameters(e)
508
509        if not qs.query.select_related:
510            qs = self.apply_select_related(qs)
511
512        # Set ordering.
513        ordering = self.get_ordering(request, qs)
514        qs = qs.order_by(*ordering)
515
516        # Apply search results
517        qs, search_use_distinct = self.model_admin.get_search_results(
518            request, qs, self.query
519        )
520
521        # Remove duplicates from results, if necessary
522        if filters_use_distinct | search_use_distinct:
523            return qs.distinct()
524        else:
525            return qs
526
527    def apply_select_related(self, qs):
528        if self.list_select_related is True:
529            return qs.select_related()
530
531        if self.list_select_related is False:
532            if self.has_related_field_in_list_display():
533                return qs.select_related()
534
535        if self.list_select_related:
536            return qs.select_related(*self.list_select_related)
537        return qs
538
539    def has_related_field_in_list_display(self):
540        for field_name in self.list_display:
541            try:
542                field = self.lookup_opts.get_field(field_name)
543            except FieldDoesNotExist:
544                pass
545            else:
546                if isinstance(field.remote_field, models.ManyToOneRel):
547                    # <FK>_id field names don't require a join.
548                    if field_name != field.get_attname():
549                        return True
550        return False
551
552    def url_for_result(self, result):
553        pk = getattr(result, self.pk_attname)
554        return reverse(
555            "admin:%s_%s_change" % (self.opts.app_label, self.opts.model_name),
556            args=(quote(pk),),
557            current_app=self.model_admin.admin_site.name,
558        )

contrib/admin/templatetags/admin_list.py

  1import datetime
  2
  3from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
  4from django.contrib.admin.utils import (
  5    display_for_field,
  6    display_for_value,
  7    label_for_field,
  8    lookup_field,
  9)
 10from django.contrib.admin.views.main import (
 11    ALL_VAR,
 12    ORDER_VAR,
 13    PAGE_VAR,
 14    SEARCH_VAR,
 15)
 16from django.core.exceptions import ObjectDoesNotExist
 17from django.db import models
 18from django.template import Library
 19from django.template.loader import get_template
 20from django.templatetags.static import static
 21from django.urls import NoReverseMatch
 22from django.utils import formats
 23from django.utils.html import format_html
 24from django.utils.safestring import mark_safe
 25from django.utils.text import capfirst
 26from django.utils.translation import gettext as _
 27
 28from .base import InclusionAdminNode
 29
 30register = Library()
 31
 32DOT = "."
 33
 34
 35@register.simple_tag
 36def paginator_number(cl, i):
 37    """
 38    Generate an individual page index link in a paginated list.
 39    """
 40    if i == DOT:
 41        return "… "
 42    elif i == cl.page_num:
 43        return format_html('<span class="this-page">{}</span> ', i + 1)
 44    else:
 45        return format_html(
 46            '<a href="{}"{}>{}</a> ',
 47            cl.get_query_string({PAGE_VAR: i}),
 48            mark_safe(' class="end"' if i == cl.paginator.num_pages - 1 else ""),
 49            i + 1,
 50        )
 51
 52
 53def pagination(cl):
 54    """
 55    Generate the series of links to the pages in a paginated list.
 56    """
 57    paginator, page_num = cl.paginator, cl.page_num
 58
 59    pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page
 60    if not pagination_required:
 61        page_range = []
 62    else:
 63        ON_EACH_SIDE = 3
 64        ON_ENDS = 2
 65
 66        # If there are 10 or fewer pages, display links to every page.
 67        # Otherwise, do some fancy
 68        if paginator.num_pages <= 10:
 69            page_range = range(paginator.num_pages)
 70        else:
 71            # Insert "smart" pagination links, so that there are always ON_ENDS
 72            # links at either end of the list of pages, and there are always
 73            # ON_EACH_SIDE links at either end of the "current page" link.
 74            page_range = []
 75            if page_num > (ON_EACH_SIDE + ON_ENDS):
 76                page_range += [
 77                    *range(0, ON_ENDS),
 78                    DOT,
 79                    *range(page_num - ON_EACH_SIDE, page_num + 1),
 80                ]
 81            else:
 82                page_range.extend(range(0, page_num + 1))
 83            if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1):
 84                page_range += [
 85                    *range(page_num + 1, page_num + ON_EACH_SIDE + 1),
 86                    DOT,
 87                    *range(paginator.num_pages - ON_ENDS, paginator.num_pages),
 88                ]
 89            else:
 90                page_range.extend(range(page_num + 1, paginator.num_pages))
 91
 92    need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page
 93    return {
 94        "cl": cl,
 95        "pagination_required": pagination_required,
 96        "show_all_url": need_show_all_link and cl.get_query_string({ALL_VAR: ""}),
 97        "page_range": page_range,
 98        "ALL_VAR": ALL_VAR,
 99        "1": 1,
100    }
101
102
103@register.tag(name="pagination")
104def pagination_tag(parser, token):
105    return InclusionAdminNode(
106        parser,
107        token,
108        func=pagination,
109        template_name="pagination.html",
110        takes_context=False,
111    )
112
113
114def result_headers(cl):
115    """
116    Generate the list column headers.
117    """
118    ordering_field_columns = cl.get_ordering_field_columns()
119    for i, field_name in enumerate(cl.list_display):
120        text, attr = label_for_field(
121            field_name, cl.model, model_admin=cl.model_admin, return_attr=True
122        )
123        is_field_sortable = cl.sortable_by is None or field_name in cl.sortable_by
124        if attr:
125            field_name = _coerce_field_name(field_name, i)
126            # Potentially not sortable
127
128            # if the field is the action checkbox: no sorting and special class
129            if field_name == "action_checkbox":
130                yield {
131                    "text": text,
132                    "class_attrib": mark_safe(' class="action-checkbox-column"'),
133                    "sortable": False,
134                }
135                continue
136
137            admin_order_field = getattr(attr, "admin_order_field", None)
138            # Set ordering for attr that is a property, if defined.
139            if isinstance(attr, property) and hasattr(attr, "fget"):
140                admin_order_field = getattr(attr.fget, "admin_order_field", None)
141            if not admin_order_field:
142                is_field_sortable = False
143
144        if not is_field_sortable:
145            # Not sortable
146            yield {
147                "text": text,
148                "class_attrib": format_html(' class="column-{}"', field_name),
149                "sortable": False,
150            }
151            continue
152
153        # OK, it is sortable if we got this far
154        th_classes = ["sortable", "column-{}".format(field_name)]
155        order_type = ""
156        new_order_type = "asc"
157        sort_priority = 0
158        # Is it currently being sorted on?
159        is_sorted = i in ordering_field_columns
160        if is_sorted:
161            order_type = ordering_field_columns.get(i).lower()
162            sort_priority = list(ordering_field_columns).index(i) + 1
163            th_classes.append("sorted %sending" % order_type)
164            new_order_type = {"asc": "desc", "desc": "asc"}[order_type]
165
166        # build new ordering param
167        o_list_primary = []  # URL for making this field the primary sort
168        o_list_remove = []  # URL for removing this field from sort
169        o_list_toggle = []  # URL for toggling order type for this field
170
171        def make_qs_param(t, n):
172            return ("-" if t == "desc" else "") + str(n)
173
174        for j, ot in ordering_field_columns.items():
175            if j == i:  # Same column
176                param = make_qs_param(new_order_type, j)
177                # We want clicking on this header to bring the ordering to the
178                # front
179                o_list_primary.insert(0, param)
180                o_list_toggle.append(param)
181                # o_list_remove - omit
182            else:
183                param = make_qs_param(ot, j)
184                o_list_primary.append(param)
185                o_list_toggle.append(param)
186                o_list_remove.append(param)
187
188        if i not in ordering_field_columns:
189            o_list_primary.insert(0, make_qs_param(new_order_type, i))
190
191        yield {
192            "text": text,
193            "sortable": True,
194            "sorted": is_sorted,
195            "ascending": order_type == "asc",
196            "sort_priority": sort_priority,
197            "url_primary": cl.get_query_string({ORDER_VAR: ".".join(o_list_primary)}),
198            "url_remove": cl.get_query_string({ORDER_VAR: ".".join(o_list_remove)}),
199            "url_toggle": cl.get_query_string({ORDER_VAR: ".".join(o_list_toggle)}),
200            "class_attrib": format_html(' class="{}"', " ".join(th_classes))
201            if th_classes
202            else "",
203        }
204
205
206def _boolean_icon(field_val):
207    icon_url = static(
208        "admin/img/icon-%s.svg" % {True: "yes", False: "no", None: "unknown"}[field_val]
209    )
210    return format_html('<img src="{}" alt="{}">', icon_url, field_val)
211
212
213def _coerce_field_name(field_name, field_index):
214    """
215    Coerce a field_name (which may be a callable) to a string.
216    """
217    if callable(field_name):
218        if field_name.__name__ == "<lambda>":
219            return "lambda" + str(field_index)
220        else:
221            return field_name.__name__
222    return field_name
223
224
225def items_for_result(cl, result, form):
226    """
227    Generate the actual list of data.
228    """
229
230    def link_in_col(is_first, field_name, cl):
231        if cl.list_display_links is None:
232            return False
233        if is_first and not cl.list_display_links:
234            return True
235        return field_name in cl.list_display_links
236
237    first = True
238    pk = cl.lookup_opts.pk.attname
239    for field_index, field_name in enumerate(cl.list_display):
240        empty_value_display = cl.model_admin.get_empty_value_display()
241        row_classes = ["field-%s" % _coerce_field_name(field_name, field_index)]
242        try:
243            f, attr, value = lookup_field(field_name, result, cl.model_admin)
244        except ObjectDoesNotExist:
245            result_repr = empty_value_display
246        else:
247            empty_value_display = getattr(
248                attr, "empty_value_display", empty_value_display
249            )
250            if f is None or f.auto_created:
251                if field_name == "action_checkbox":
252                    row_classes = ["action-checkbox"]
253                boolean = getattr(attr, "boolean", False)
254                result_repr = display_for_value(value, empty_value_display, boolean)
255                if isinstance(value, (datetime.date, datetime.time)):
256                    row_classes.append("nowrap")
257            else:
258                if isinstance(f.remote_field, models.ManyToOneRel):
259                    field_val = getattr(result, f.name)
260                    if field_val is None:
261                        result_repr = empty_value_display
262                    else:
263                        result_repr = field_val
264                else:
265                    result_repr = display_for_field(value, f, empty_value_display)
266                if isinstance(
267                    f, (models.DateField, models.TimeField, models.ForeignKey)
268                ):
269                    row_classes.append("nowrap")
270        if str(result_repr) == "":
271            result_repr = mark_safe("&nbsp;")
272        row_class = mark_safe(' class="%s"' % " ".join(row_classes))
273        # If list_display_links not defined, add the link tag to the first field
274        if link_in_col(first, field_name, cl):
275            table_tag = "th" if first else "td"
276            first = False
277
278            # Display link to the result's change_view if the url exists, else
279            # display just the result's representation.
280            try:
281                url = cl.url_for_result(result)
282            except NoReverseMatch:
283                link_or_text = result_repr
284            else:
285                url = add_preserved_filters(
286                    {"preserved_filters": cl.preserved_filters, "opts": cl.opts}, url
287                )
288                # Convert the pk to something that can be used in Javascript.
289                # Problem cases are non-ASCII strings.
290                if cl.to_field:
291                    attr = str(cl.to_field)
292                else:
293                    attr = pk
294                value = result.serializable_value(attr)
295                link_or_text = format_html(
296                    '<a href="{}"{}>{}</a>',
297                    url,
298                    format_html(' data-popup-opener="{}"', value)
299                    if cl.is_popup
300                    else "",
301                    result_repr,
302                )
303
304            yield format_html(
305                "<{}{}>{}</{}>", table_tag, row_class, link_or_text, table_tag
306            )
307        else:
308            # By default the fields come from ModelAdmin.list_editable, but if we pull
309            # the fields out of the form instead of list_editable custom admins
310            # can provide fields on a per request basis
311            if (
312                form
313                and field_name in form.fields
314                and not (
315                    field_name == cl.model._meta.pk.name
316                    and form[cl.model._meta.pk.name].is_hidden
317                )
318            ):
319                bf = form[field_name]
320                result_repr = mark_safe(str(bf.errors) + str(bf))
321            yield format_html("<td{}>{}</td>", row_class, result_repr)
322    if form and not form[cl.model._meta.pk.name].is_hidden:
323        yield format_html("<td>{}</td>", form[cl.model._meta.pk.name])
324
325
326class ResultList(list):
327    """
328    Wrapper class used to return items in a list_editable changelist, annotated
329    with the form object for error reporting purposes. Needed to maintain
330    backwards compatibility with existing admin templates.
331    """
332
333    def __init__(self, form, *items):
334        self.form = form
335        super().__init__(*items)
336
337
338def results(cl):
339    if cl.formset:
340        for res, form in zip(cl.result_list, cl.formset.forms):
341            yield ResultList(form, items_for_result(cl, res, form))
342    else:
343        for res in cl.result_list:
344            yield ResultList(None, items_for_result(cl, res, None))
345
346
347def result_hidden_fields(cl):
348    if cl.formset:
349        for res, form in zip(cl.result_list, cl.formset.forms):
350            if form[cl.model._meta.pk.name].is_hidden:
351                yield mark_safe(form[cl.model._meta.pk.name])
352
353
354def result_list(cl):
355    """
356    Display the headers and data list together.
357    """
358    headers = list(result_headers(cl))
359    num_sorted_fields = 0
360    for h in headers:
361        if h["sortable"] and h["sorted"]:
362            num_sorted_fields += 1
363    return {
364        "cl": cl,
365        "result_hidden_fields": list(result_hidden_fields(cl)),
366        "result_headers": headers,
367        "num_sorted_fields": num_sorted_fields,
368        "results": list(results(cl)),
369    }
370
371
372@register.tag(name="result_list")
373def result_list_tag(parser, token):
374    return InclusionAdminNode(
375        parser,
376        token,
377        func=result_list,
378        template_name="change_list_results.html",
379        takes_context=False,
380    )
381
382
383def date_hierarchy(cl):
384    """
385    Display the date hierarchy for date drill-down functionality.
386    """
387    if cl.date_hierarchy:
388        field_name = cl.date_hierarchy
389        year_field = "%s__year" % field_name
390        month_field = "%s__month" % field_name
391        day_field = "%s__day" % field_name
392        field_generic = "%s__" % field_name
393        year_lookup = cl.params.get(year_field)
394        month_lookup = cl.params.get(month_field)
395        day_lookup = cl.params.get(day_field)
396
397        def link(filters):
398            return cl.get_query_string(filters, [field_generic])
399
400        if not (year_lookup or month_lookup or day_lookup):
401            # select appropriate start level
402            date_range = cl.queryset.aggregate(
403                first=models.Min(field_name), last=models.Max(field_name)
404            )
405            if date_range["first"] and date_range["last"]:
406                if date_range["first"].year == date_range["last"].year:
407                    year_lookup = date_range["first"].year
408                    if date_range["first"].month == date_range["last"].month:
409                        month_lookup = date_range["first"].month
410
411        if year_lookup and month_lookup and day_lookup:
412            day = datetime.date(int(year_lookup), int(month_lookup), int(day_lookup))
413            return {
414                "show": True,
415                "back": {
416                    "link": link({year_field: year_lookup, month_field: month_lookup}),
417                    "title": capfirst(formats.date_format(day, "YEAR_MONTH_FORMAT")),
418                },
419                "choices": [
420                    {"title": capfirst(formats.date_format(day, "MONTH_DAY_FORMAT"))}
421                ],
422            }
423        elif year_lookup and month_lookup:
424            days = getattr(cl.queryset, "dates")(field_name, "day")
425            return {
426                "show": True,
427                "back": {
428                    "link": link({year_field: year_lookup}),
429                    "title": str(year_lookup),
430                },
431                "choices": [
432                    {
433                        "link": link(
434                            {
435                                year_field: year_lookup,
436                                month_field: month_lookup,
437                                day_field: day.day,
438                            }
439                        ),
440                        "title": capfirst(formats.date_format(day, "MONTH_DAY_FORMAT")),
441                    }
442                    for day in days
443                ],
444            }
445        elif year_lookup:
446            months = getattr(cl.queryset, "dates")(field_name, "month")
447            return {
448                "show": True,
449                "back": {"link": link({}), "title": _("All dates")},
450                "choices": [
451                    {
452                        "link": link(
453                            {year_field: year_lookup, month_field: month.month}
454                        ),
455                        "title": capfirst(
456                            formats.date_format(month, "YEAR_MONTH_FORMAT")
457                        ),
458                    }
459                    for month in months
460                ],
461            }
462        else:
463            years = getattr(cl.queryset, "dates")(field_name, "year")
464            return {
465                "show": True,
466                "back": None,
467                "choices": [
468                    {
469                        "link": link({year_field: str(year.year)}),
470                        "title": str(year.year),
471                    }
472                    for year in years
473                ],
474            }
475
476
477@register.tag(name="date_hierarchy")
478def date_hierarchy_tag(parser, token):
479    return InclusionAdminNode(
480        parser,
481        token,
482        func=date_hierarchy,
483        template_name="date_hierarchy.html",
484        takes_context=False,
485    )
486
487
488def search_form(cl):
489    """
490    Display a search form for searching the list.
491    """
492    return {
493        "cl": cl,
494        "show_result_count": cl.result_count != cl.full_result_count,
495        "search_var": SEARCH_VAR,
496    }
497
498
499@register.tag(name="search_form")
500def search_form_tag(parser, token):
501    return InclusionAdminNode(
502        parser,
503        token,
504        func=search_form,
505        template_name="search_form.html",
506        takes_context=False,
507    )
508
509
510@register.simple_tag
511def admin_list_filter(cl, spec):
512    tpl = get_template(spec.template)
513    return tpl.render(
514        {"title": spec.title, "choices": list(spec.choices(cl)), "spec": spec,}
515    )
516
517
518def admin_actions(context):
519    """
520    Track the number of times the action field has been rendered on the page,
521    so we know which value to use.
522    """
523    context["action_index"] = context.get("action_index", -1) + 1
524    return context
525
526
527@register.tag(name="admin_actions")
528def admin_actions_tag(parser, token):
529    return InclusionAdminNode(
530        parser, token, func=admin_actions, template_name="actions.html"
531    )
532
533
534@register.tag(name="change_list_object_tools")
535def change_list_object_tools_tag(parser, token):
536    """Display the row of change list object tools."""
537    return InclusionAdminNode(
538        parser,
539        token,
540        func=lambda context: context,
541        template_name="change_list_object_tools.html",
542    )

tests/admin_views/models.py

   1import datetime
   2import os
   3import tempfile
   4import uuid
   5
   6from django.contrib.auth.models import User
   7from django.contrib.contenttypes.fields import (
   8    GenericForeignKey,
   9    GenericRelation,
  10)
  11from django.contrib.contenttypes.models import ContentType
  12from django.core.exceptions import ValidationError
  13from django.core.files.storage import FileSystemStorage
  14from django.db import models
  15
  16
  17class Section(models.Model):
  18    """
  19    A simple section that links to articles, to test linking to related items
  20    in admin views.
  21    """
  22
  23    name = models.CharField(max_length=100)
  24
  25    def __str__(self):
  26        return self.name
  27
  28    @property
  29    def name_property(self):
  30        """
  31        A property that simply returns the name. Used to test #24461
  32        """
  33        return self.name
  34
  35
  36class Article(models.Model):
  37    """
  38    A simple article to test admin views. Test backwards compatibility.
  39    """
  40
  41    title = models.CharField(max_length=100)
  42    content = models.TextField()
  43    date = models.DateTimeField()
  44    section = models.ForeignKey(Section, models.CASCADE, null=True, blank=True)
  45    another_section = models.ForeignKey(
  46        Section, models.CASCADE, null=True, blank=True, related_name="+"
  47    )
  48    sub_section = models.ForeignKey(
  49        Section, models.SET_NULL, null=True, blank=True, related_name="+"
  50    )
  51
  52    def __str__(self):
  53        return self.title
  54
  55    def model_year(self):
  56        return self.date.year
  57
  58    model_year.admin_order_field = "date"
  59    model_year.short_description = ""
  60
  61    def model_year_reversed(self):
  62        return self.date.year
  63
  64    model_year_reversed.admin_order_field = "-date"
  65    model_year_reversed.short_description = ""
  66
  67    def property_year(self):
  68        return self.date.year
  69
  70    property_year.admin_order_field = "date"
  71    model_property_year = property(property_year)
  72
  73    @property
  74    def model_month(self):
  75        return self.date.month
  76
  77
  78class Book(models.Model):
  79    """
  80    A simple book that has chapters.
  81    """
  82
  83    name = models.CharField(max_length=100, verbose_name="¿Name?")
  84
  85    def __str__(self):
  86        return self.name
  87
  88
  89class Promo(models.Model):
  90    name = models.CharField(max_length=100, verbose_name="¿Name?")
  91    book = models.ForeignKey(Book, models.CASCADE)
  92    author = models.ForeignKey(User, models.SET_NULL, blank=True, null=True)
  93
  94    def __str__(self):
  95        return self.name
  96
  97
  98class Chapter(models.Model):
  99    title = models.CharField(max_length=100, verbose_name="¿Title?")
 100    content = models.TextField()
 101    book = models.ForeignKey(Book, models.CASCADE)
 102
 103    class Meta:
 104        # Use a utf-8 bytestring to ensure it works (see #11710)
 105        verbose_name = "¿Chapter?"
 106
 107    def __str__(self):
 108        return self.title
 109
 110
 111class ChapterXtra1(models.Model):
 112    chap = models.OneToOneField(Chapter, models.CASCADE, verbose_name="¿Chap?")
 113    xtra = models.CharField(max_length=100, verbose_name="¿Xtra?")
 114    guest_author = models.ForeignKey(User, models.SET_NULL, blank=True, null=True)
 115
 116    def __str__(self):
 117        return "¿Xtra1: %s" % self.xtra
 118
 119
 120class ChapterXtra2(models.Model):
 121    chap = models.OneToOneField(Chapter, models.CASCADE, verbose_name="¿Chap?")
 122    xtra = models.CharField(max_length=100, verbose_name="¿Xtra?")
 123
 124    def __str__(self):
 125        return "¿Xtra2: %s" % self.xtra
 126
 127
 128class RowLevelChangePermissionModel(models.Model):
 129    name = models.CharField(max_length=100, blank=True)
 130
 131
 132class CustomArticle(models.Model):
 133    content = models.TextField()
 134    date = models.DateTimeField()
 135
 136
 137class ModelWithStringPrimaryKey(models.Model):
 138    string_pk = models.CharField(max_length=255, primary_key=True)
 139
 140    def __str__(self):
 141        return self.string_pk
 142
 143    def get_absolute_url(self):
 144        return "/dummy/%s/" % self.string_pk
 145
 146
 147class Color(models.Model):
 148    value = models.CharField(max_length=10)
 149    warm = models.BooleanField(default=False)
 150
 151    def __str__(self):
 152        return self.value
 153
 154
 155# we replicate Color to register with another ModelAdmin
 156class Color2(Color):
 157    class Meta:
 158        proxy = True
 159
 160
 161class Thing(models.Model):
 162    title = models.CharField(max_length=20)
 163    color = models.ForeignKey(Color, models.CASCADE, limit_choices_to={"warm": True})
 164    pub_date = models.DateField(blank=True, null=True)
 165
 166    def __str__(self):
 167        return self.title
 168
 169
 170class Actor(models.Model):
 171    name = models.CharField(max_length=50)
 172    age = models.IntegerField()
 173    title = models.CharField(max_length=50, null=True, blank=True)
 174
 175    def __str__(self):
 176        return self.name
 177
 178
 179class Inquisition(models.Model):
 180    expected = models.BooleanField(default=False)
 181    leader = models.ForeignKey(Actor, models.CASCADE)
 182    country = models.CharField(max_length=20)
 183
 184    def __str__(self):
 185        return "by %s from %s" % (self.leader, self.country)
 186
 187
 188class Sketch(models.Model):
 189    title = models.CharField(max_length=100)
 190    inquisition = models.ForeignKey(
 191        Inquisition,
 192        models.CASCADE,
 193        limit_choices_to={
 194            "leader__name": "Palin",
 195            "leader__age": 27,
 196            "expected": False,
 197        },
 198    )
 199    defendant0 = models.ForeignKey(
 200        Actor,
 201        models.CASCADE,
 202        limit_choices_to={"title__isnull": False},
 203        related_name="as_defendant0",
 204    )
 205    defendant1 = models.ForeignKey(
 206        Actor,
 207        models.CASCADE,
 208        limit_choices_to={"title__isnull": True},
 209        related_name="as_defendant1",
 210    )
 211
 212    def __str__(self):
 213        return self.title
 214
 215
 216def today_callable_dict():
 217    return {"last_action__gte": datetime.datetime.today()}
 218
 219
 220def today_callable_q():
 221    return models.Q(last_action__gte=datetime.datetime.today())
 222
 223
 224class Character(models.Model):
 225    username = models.CharField(max_length=100)
 226    last_action = models.DateTimeField()
 227
 228    def __str__(self):
 229        return self.username
 230
 231
 232class StumpJoke(models.Model):
 233    variation = models.CharField(max_length=100)
 234    most_recently_fooled = models.ForeignKey(
 235        Character,
 236        models.CASCADE,
 237        limit_choices_to=today_callable_dict,
 238        related_name="+",
 239    )
 240    has_fooled_today = models.ManyToManyField(
 241        Character, limit_choices_to=today_callable_q, related_name="+"
 242    )
 243
 244    def __str__(self):
 245        return self.variation
 246
 247
 248class Fabric(models.Model):
 249    NG_CHOICES = (
 250        ("Textured", (("x", "Horizontal"), ("y", "Vertical"),)),
 251        ("plain", "Smooth"),
 252    )
 253    surface = models.CharField(max_length=20, choices=NG_CHOICES)
 254
 255
 256class Person(models.Model):
 257    GENDER_CHOICES = (
 258        (1, "Male"),
 259        (2, "Female"),
 260    )
 261    name = models.CharField(max_length=100)
 262    gender = models.IntegerField(choices=GENDER_CHOICES)
 263    age = models.IntegerField(default=21)
 264    alive = models.BooleanField(default=True)
 265
 266    def __str__(self):
 267        return self.name
 268
 269
 270class Persona(models.Model):
 271    """
 272    A simple persona associated with accounts, to test inlining of related
 273    accounts which inherit from a common accounts class.
 274    """
 275
 276    name = models.CharField(blank=False, max_length=80)
 277
 278    def __str__(self):
 279        return self.name
 280
 281
 282class Account(models.Model):
 283    """
 284    A simple, generic account encapsulating the information shared by all
 285    types of accounts.
 286    """
 287
 288    username = models.CharField(blank=False, max_length=80)
 289    persona = models.ForeignKey(Persona, models.CASCADE, related_name="accounts")
 290    servicename = "generic service"
 291
 292    def __str__(self):
 293        return "%s: %s" % (self.servicename, self.username)
 294
 295
 296class FooAccount(Account):
 297    """A service-specific account of type Foo."""
 298
 299    servicename = "foo"
 300
 301
 302class BarAccount(Account):
 303    """A service-specific account of type Bar."""
 304
 305    servicename = "bar"
 306
 307
 308class Subscriber(models.Model):
 309    name = models.CharField(blank=False, max_length=80)
 310    email = models.EmailField(blank=False, max_length=175)
 311
 312    def __str__(self):
 313        return "%s (%s)" % (self.name, self.email)
 314
 315
 316class ExternalSubscriber(Subscriber):
 317    pass
 318
 319
 320class OldSubscriber(Subscriber):
 321    pass
 322
 323
 324class Media(models.Model):
 325    name = models.CharField(max_length=60)
 326
 327
 328class Podcast(Media):
 329    release_date = models.DateField()
 330
 331    class Meta:
 332        ordering = ("release_date",)  # overridden in PodcastAdmin
 333
 334
 335class Vodcast(Media):
 336    media = models.OneToOneField(
 337        Media, models.CASCADE, primary_key=True, parent_link=True
 338    )
 339    released = models.BooleanField(default=False)
 340
 341
 342class Parent(models.Model):
 343    name = models.CharField(max_length=128)
 344
 345    def clean(self):
 346        if self.name == "_invalid":
 347            raise ValidationError("invalid")
 348
 349
 350class Child(models.Model):
 351    parent = models.ForeignKey(Parent, models.CASCADE, editable=False)
 352    name = models.CharField(max_length=30, blank=True)
 353
 354    def clean(self):
 355        if self.name == "_invalid":
 356            raise ValidationError("invalid")
 357
 358
 359class EmptyModel(models.Model):
 360    def __str__(self):
 361        return "Primary key = %s" % self.id
 362
 363
 364temp_storage = FileSystemStorage(tempfile.mkdtemp())
 365UPLOAD_TO = os.path.join(temp_storage.location, "test_upload")
 366
 367
 368class Gallery(models.Model):
 369    name = models.CharField(max_length=100)
 370
 371
 372class Picture(models.Model):
 373    name = models.CharField(max_length=100)
 374    image = models.FileField(storage=temp_storage, upload_to="test_upload")
 375    gallery = models.ForeignKey(Gallery, models.CASCADE, related_name="pictures")
 376
 377
 378class Language(models.Model):
 379    iso = models.CharField(max_length=5, primary_key=True)
 380    name = models.CharField(max_length=50)
 381    english_name = models.CharField(max_length=50)
 382    shortlist = models.BooleanField(default=False)
 383
 384    class Meta:
 385        ordering = ("iso",)
 386
 387
 388# a base class for Recommender and Recommendation
 389class Title(models.Model):
 390    pass
 391
 392
 393class TitleTranslation(models.Model):
 394    title = models.ForeignKey(Title, models.CASCADE)
 395    text = models.CharField(max_length=100)
 396
 397
 398class Recommender(Title):
 399    pass
 400
 401
 402class Recommendation(Title):
 403    the_recommender = models.ForeignKey(Recommender, models.CASCADE)
 404
 405
 406class Collector(models.Model):
 407    name = models.CharField(max_length=100)
 408
 409
 410class Widget(models.Model):
 411    owner = models.ForeignKey(Collector, models.CASCADE)
 412    name = models.CharField(max_length=100)
 413
 414
 415class DooHickey(models.Model):
 416    code = models.CharField(max_length=10, primary_key=True)
 417    owner = models.ForeignKey(Collector, models.CASCADE)
 418    name = models.CharField(max_length=100)
 419
 420
 421class Grommet(models.Model):
 422    code = models.AutoField(primary_key=True)
 423    owner = models.ForeignKey(Collector, models.CASCADE)
 424    name = models.CharField(max_length=100)
 425
 426
 427class Whatsit(models.Model):
 428    index = models.IntegerField(primary_key=True)
 429    owner = models.ForeignKey(Collector, models.CASCADE)
 430    name = models.CharField(max_length=100)
 431
 432
 433class Doodad(models.Model):
 434    name = models.CharField(max_length=100)
 435
 436
 437class FancyDoodad(Doodad):
 438    owner = models.ForeignKey(Collector, models.CASCADE)
 439    expensive = models.BooleanField(default=True)
 440
 441
 442class Category(models.Model):
 443    collector = models.ForeignKey(Collector, models.CASCADE)
 444    order = models.PositiveIntegerField()
 445
 446    class Meta:
 447        ordering = ("order",)
 448
 449    def __str__(self):
 450        return "%s:o%s" % (self.id, self.order)
 451
 452
 453def link_posted_default():
 454    return datetime.date.today() - datetime.timedelta(days=7)
 455
 456
 457class Link(models.Model):
 458    posted = models.DateField(default=link_posted_default)
 459    url = models.URLField()
 460    post = models.ForeignKey("Post", models.CASCADE)
 461    readonly_link_content = models.TextField()
 462
 463
 464class PrePopulatedPost(models.Model):
 465    title = models.CharField(max_length=100)
 466    published = models.BooleanField(default=False)
 467    slug = models.SlugField()
 468
 469
 470class PrePopulatedSubPost(models.Model):
 471    post = models.ForeignKey(PrePopulatedPost, models.CASCADE)
 472    subtitle = models.CharField(max_length=100)
 473    subslug = models.SlugField()
 474
 475
 476class Post(models.Model):
 477    title = models.CharField(
 478        max_length=100, help_text="Some help text for the title (with unicode ŠĐĆŽćžšđ)"
 479    )
 480    content = models.TextField(
 481        help_text="Some help text for the content (with unicode ŠĐĆŽćžšđ)"
 482    )
 483    readonly_content = models.TextField()
 484    posted = models.DateField(
 485        default=datetime.date.today,
 486        help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)",
 487    )
 488    public = models.BooleanField(null=True, blank=True)
 489
 490    def awesomeness_level(self):
 491        return "Very awesome."
 492
 493
 494# Proxy model to test overridden fields attrs on Post model so as not to
 495# interfere with other tests.
 496class FieldOverridePost(Post):
 497    class Meta:
 498        proxy = True
 499
 500
 501class Gadget(models.Model):
 502    name = models.CharField(max_length=100)
 503
 504    def __str__(self):
 505        return self.name
 506
 507
 508class Villain(models.Model):
 509    name = models.CharField(max_length=100)
 510
 511    def __str__(self):
 512        return self.name
 513
 514
 515class SuperVillain(Villain):
 516    pass
 517
 518
 519class FunkyTag(models.Model):
 520    "Because we all know there's only one real use case for GFKs."
 521    name = models.CharField(max_length=25)
 522    content_type = models.ForeignKey(ContentType, models.CASCADE)
 523    object_id = models.PositiveIntegerField()
 524    content_object = GenericForeignKey("content_type", "object_id")
 525
 526    def __str__(self):
 527        return self.name
 528
 529
 530class Plot(models.Model):
 531    name = models.CharField(max_length=100)
 532    team_leader = models.ForeignKey(Villain, models.CASCADE, related_name="lead_plots")
 533    contact = models.ForeignKey(Villain, models.CASCADE, related_name="contact_plots")
 534    tags = GenericRelation(FunkyTag)
 535
 536    def __str__(self):
 537        return self.name
 538
 539
 540class PlotDetails(models.Model):
 541    details = models.CharField(max_length=100)
 542    plot = models.OneToOneField(Plot, models.CASCADE, null=True, blank=True)
 543
 544    def __str__(self):
 545        return self.details
 546
 547
 548class PlotProxy(Plot):
 549    class Meta:
 550        proxy = True
 551
 552
 553class SecretHideout(models.Model):
 554    """ Secret! Not registered with the admin! """
 555
 556    location = models.CharField(max_length=100)
 557    villain = models.ForeignKey(Villain, models.CASCADE)
 558
 559    def __str__(self):
 560        return self.location
 561
 562
 563class SuperSecretHideout(models.Model):
 564    """ Secret! Not registered with the admin! """
 565
 566    location = models.CharField(max_length=100)
 567    supervillain = models.ForeignKey(SuperVillain, models.CASCADE)
 568
 569    def __str__(self):
 570        return self.location
 571
 572
 573class Bookmark(models.Model):
 574    name = models.CharField(max_length=60)
 575    tag = GenericRelation(FunkyTag, related_query_name="bookmark")
 576
 577    def __str__(self):
 578        return self.name
 579
 580
 581class CyclicOne(models.Model):
 582    name = models.CharField(max_length=25)
 583    two = models.ForeignKey("CyclicTwo", models.CASCADE)
 584
 585    def __str__(self):
 586        return self.name
 587
 588
 589class CyclicTwo(models.Model):
 590    name = models.CharField(max_length=25)
 591    one = models.ForeignKey(CyclicOne, models.CASCADE)
 592
 593    def __str__(self):
 594        return self.name
 595
 596
 597class Topping(models.Model):
 598    name = models.CharField(max_length=20)
 599
 600    def __str__(self):
 601        return self.name
 602
 603
 604class Pizza(models.Model):
 605    name = models.CharField(max_length=20)
 606    toppings = models.ManyToManyField("Topping", related_name="pizzas")
 607
 608
 609# Pizza's ModelAdmin has readonly_fields = ['toppings'].
 610# toppings is editable for this model's admin.
 611class ReadablePizza(Pizza):
 612    class Meta:
 613        proxy = True
 614
 615
 616# No default permissions are created for this model and both name and toppings
 617# are readonly for this model's admin.
 618class ReadOnlyPizza(Pizza):
 619    class Meta:
 620        proxy = True
 621        default_permissions = ()
 622
 623
 624class Album(models.Model):
 625    owner = models.ForeignKey(User, models.SET_NULL, null=True, blank=True)
 626    title = models.CharField(max_length=30)
 627
 628
 629class Song(models.Model):
 630    name = models.CharField(max_length=20)
 631    album = models.ForeignKey(Album, on_delete=models.RESTRICT)
 632
 633    def __str__(self):
 634        return self.name
 635
 636
 637class Employee(Person):
 638    code = models.CharField(max_length=20)
 639
 640
 641class WorkHour(models.Model):
 642    datum = models.DateField()
 643    employee = models.ForeignKey(Employee, models.CASCADE)
 644
 645
 646class Question(models.Model):
 647    question = models.CharField(max_length=20)
 648    posted = models.DateField(default=datetime.date.today)
 649    expires = models.DateTimeField(null=True, blank=True)
 650    related_questions = models.ManyToManyField("self")
 651
 652    def __str__(self):
 653        return self.question
 654
 655
 656class Answer(models.Model):
 657    question = models.ForeignKey(Question, models.PROTECT)
 658    answer = models.CharField(max_length=20)
 659
 660    def __str__(self):
 661        return self.answer
 662
 663
 664class Answer2(Answer):
 665    class Meta:
 666        proxy = True
 667
 668
 669class Reservation(models.Model):
 670    start_date = models.DateTimeField()
 671    price = models.IntegerField()
 672
 673
 674class FoodDelivery(models.Model):
 675    DRIVER_CHOICES = (
 676        ("bill", "Bill G"),
 677        ("steve", "Steve J"),
 678    )
 679    RESTAURANT_CHOICES = (
 680        ("indian", "A Taste of India"),
 681        ("thai", "Thai Pography"),
 682        ("pizza", "Pizza Mama"),
 683    )
 684    reference = models.CharField(max_length=100)
 685    driver = models.CharField(max_length=100, choices=DRIVER_CHOICES, blank=True)
 686    restaurant = models.CharField(
 687        max_length=100, choices=RESTAURANT_CHOICES, blank=True
 688    )
 689
 690    class Meta:
 691        unique_together = (("driver", "restaurant"),)
 692
 693
 694class CoverLetter(models.Model):
 695    author = models.CharField(max_length=30)
 696    date_written = models.DateField(null=True, blank=True)
 697
 698    def __str__(self):
 699        return self.author
 700
 701
 702class Paper(models.Model):
 703    title = models.CharField(max_length=30)
 704    author = models.CharField(max_length=30, blank=True, null=True)
 705
 706
 707class ShortMessage(models.Model):
 708    content = models.CharField(max_length=140)
 709    timestamp = models.DateTimeField(null=True, blank=True)
 710
 711
 712class Telegram(models.Model):
 713    title = models.CharField(max_length=30)
 714    date_sent = models.DateField(null=True, blank=True)
 715
 716    def __str__(self):
 717        return self.title
 718
 719
 720class Story(models.Model):
 721    title = models.CharField(max_length=100)
 722    content = models.TextField()
 723
 724
 725class OtherStory(models.Model):
 726    title = models.CharField(max_length=100)
 727    content = models.TextField()
 728
 729
 730class ComplexSortedPerson(models.Model):
 731    name = models.CharField(max_length=100)
 732    age = models.PositiveIntegerField()
 733    is_employee = models.BooleanField(null=True)
 734
 735
 736class PluggableSearchPerson(models.Model):
 737    name = models.CharField(max_length=100)
 738    age = models.PositiveIntegerField()
 739
 740
 741class PrePopulatedPostLargeSlug(models.Model):
 742    """
 743    Regression test for #15938: a large max_length for the slugfield must not
 744    be localized in prepopulated_fields_js.html or it might end up breaking
 745    the javascript (ie, using THOUSAND_SEPARATOR ends up with maxLength=1,000)
 746    """
 747
 748    title = models.CharField(max_length=100)
 749    published = models.BooleanField(default=False)
 750    # `db_index=False` because MySQL cannot index large CharField (#21196).
 751    slug = models.SlugField(max_length=1000, db_index=False)
 752
 753
 754class AdminOrderedField(models.Model):
 755    order = models.IntegerField()
 756    stuff = models.CharField(max_length=200)
 757
 758
 759class AdminOrderedModelMethod(models.Model):
 760    order = models.IntegerField()
 761    stuff = models.CharField(max_length=200)
 762
 763    def some_order(self):
 764        return self.order
 765
 766    some_order.admin_order_field = "order"
 767
 768
 769class AdminOrderedAdminMethod(models.Model):
 770    order = models.IntegerField()
 771    stuff = models.CharField(max_length=200)
 772
 773
 774class AdminOrderedCallable(models.Model):
 775    order = models.IntegerField()
 776    stuff = models.CharField(max_length=200)
 777
 778
 779class Report(models.Model):
 780    title = models.CharField(max_length=100)
 781
 782    def __str__(self):
 783        return self.title
 784
 785
 786class MainPrepopulated(models.Model):
 787    name = models.CharField(max_length=100)
 788    pubdate = models.DateField()
 789    status = models.CharField(
 790        max_length=20,
 791        choices=(("option one", "Option One"), ("option two", "Option Two")),
 792    )
 793    slug1 = models.SlugField(blank=True)
 794    slug2 = models.SlugField(blank=True)
 795    slug3 = models.SlugField(blank=True, allow_unicode=True)
 796
 797
 798class RelatedPrepopulated(models.Model):
 799    parent = models.ForeignKey(MainPrepopulated, models.CASCADE)
 800    name = models.CharField(max_length=75)
 801    fk = models.ForeignKey("self", models.CASCADE, blank=True, null=True)
 802    m2m = models.ManyToManyField("self", blank=True)
 803    pubdate = models.DateField()
 804    status = models.CharField(
 805        max_length=20,
 806        choices=(("option one", "Option One"), ("option two", "Option Two")),
 807    )
 808    slug1 = models.SlugField(max_length=50)
 809    slug2 = models.SlugField(max_length=60)
 810
 811
 812class UnorderedObject(models.Model):
 813    """
 814    Model without any defined `Meta.ordering`.
 815    Refs #16819.
 816    """
 817
 818    name = models.CharField(max_length=255)
 819    bool = models.BooleanField(default=True)
 820
 821
 822class UndeletableObject(models.Model):
 823    """
 824    Model whose show_delete in admin change_view has been disabled
 825    Refs #10057.
 826    """
 827
 828    name = models.CharField(max_length=255)
 829
 830
 831class UnchangeableObject(models.Model):
 832    """
 833    Model whose change_view is disabled in admin
 834    Refs #20640.
 835    """
 836
 837
 838class UserMessenger(models.Model):
 839    """
 840    Dummy class for testing message_user functions on ModelAdmin
 841    """
 842
 843
 844class Simple(models.Model):
 845    """
 846    Simple model with nothing on it for use in testing
 847    """
 848
 849
 850class Choice(models.Model):
 851    choice = models.IntegerField(
 852        blank=True, null=True, choices=((1, "Yes"), (0, "No"), (None, "No opinion")),
 853    )
 854
 855
 856class ParentWithDependentChildren(models.Model):
 857    """
 858    Issue #20522
 859    Model where the validation of child foreign-key relationships depends
 860    on validation of the parent
 861    """
 862
 863    some_required_info = models.PositiveIntegerField()
 864    family_name = models.CharField(max_length=255, blank=False)
 865
 866
 867class DependentChild(models.Model):
 868    """
 869    Issue #20522
 870    Model that depends on validation of the parent class for one of its
 871    fields to validate during clean
 872    """
 873
 874    parent = models.ForeignKey(ParentWithDependentChildren, models.CASCADE)
 875    family_name = models.CharField(max_length=255)
 876
 877
 878class _Manager(models.Manager):
 879    def get_queryset(self):
 880        return super().get_queryset().filter(pk__gt=1)
 881
 882
 883class FilteredManager(models.Model):
 884    def __str__(self):
 885        return "PK=%d" % self.pk
 886
 887    pk_gt_1 = _Manager()
 888    objects = models.Manager()
 889
 890
 891class EmptyModelVisible(models.Model):
 892    """ See ticket #11277. """
 893
 894
 895class EmptyModelHidden(models.Model):
 896    """ See ticket #11277. """
 897
 898
 899class EmptyModelMixin(models.Model):
 900    """ See ticket #11277. """
 901
 902
 903class State(models.Model):
 904    name = models.CharField(max_length=100, verbose_name="State verbose_name")
 905
 906
 907class City(models.Model):
 908    state = models.ForeignKey(State, models.CASCADE)
 909    name = models.CharField(max_length=100, verbose_name="City verbose_name")
 910
 911    def get_absolute_url(self):
 912        return "/dummy/%s/" % self.pk
 913
 914
 915class Restaurant(models.Model):
 916    city = models.ForeignKey(City, models.CASCADE)
 917    name = models.CharField(max_length=100)
 918
 919    def get_absolute_url(self):
 920        return "/dummy/%s/" % self.pk
 921
 922
 923class Worker(models.Model):
 924    work_at = models.ForeignKey(Restaurant, models.CASCADE)
 925    name = models.CharField(max_length=50)
 926    surname = models.CharField(max_length=50)
 927
 928
 929# Models for #23329
 930class ReferencedByParent(models.Model):
 931    name = models.CharField(max_length=20, unique=True)
 932
 933
 934class ParentWithFK(models.Model):
 935    fk = models.ForeignKey(
 936        ReferencedByParent, models.CASCADE, to_field="name", related_name="hidden+",
 937    )
 938
 939
 940class ChildOfReferer(ParentWithFK):
 941    pass
 942
 943
 944# Models for #23431
 945class InlineReferer(models.Model):
 946    pass
 947
 948
 949class ReferencedByInline(models.Model):
 950    name = models.CharField(max_length=20, unique=True)
 951
 952
 953class InlineReference(models.Model):
 954    referer = models.ForeignKey(InlineReferer, models.CASCADE)
 955    fk = models.ForeignKey(
 956        ReferencedByInline, models.CASCADE, to_field="name", related_name="hidden+",
 957    )
 958
 959
 960class Recipe(models.Model):
 961    rname = models.CharField(max_length=20, unique=True)
 962
 963
 964class Ingredient(models.Model):
 965    iname = models.CharField(max_length=20, unique=True)
 966    recipes = models.ManyToManyField(Recipe, through="RecipeIngredient")
 967
 968
 969class RecipeIngredient(models.Model):
 970    ingredient = models.ForeignKey(Ingredient, models.CASCADE, to_field="iname")
 971    recipe = models.ForeignKey(Recipe, models.CASCADE, to_field="rname")
 972
 973
 974# Model for #23839
 975class NotReferenced(models.Model):
 976    # Don't point any FK at this model.
 977    pass
 978
 979
 980# Models for #23934
 981class ExplicitlyProvidedPK(models.Model):
 982    name = models.IntegerField(primary_key=True)
 983
 984
 985class ImplicitlyGeneratedPK(models.Model):
 986    name = models.IntegerField(unique=True)
 987
 988
 989# Models for #25622
 990class ReferencedByGenRel(models.Model):
 991    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
 992    object_id = models.PositiveIntegerField()
 993    content_object = GenericForeignKey("content_type", "object_id")
 994
 995
 996class GenRelReference(models.Model):
 997    references = GenericRelation(ReferencedByGenRel)
 998
 999
1000class ParentWithUUIDPK(models.Model):
1001    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
1002    title = models.CharField(max_length=100)
1003
1004    def __str__(self):
1005        return str(self.id)
1006
1007
1008class RelatedWithUUIDPKModel(models.Model):
1009    parent = models.ForeignKey(
1010        ParentWithUUIDPK, on_delete=models.SET_NULL, null=True, blank=True
1011    )
1012
1013
1014class Author(models.Model):
1015    pass
1016
1017
1018class Authorship(models.Model):
1019    book = models.ForeignKey(Book, models.CASCADE)
1020    author = models.ForeignKey(Author, models.CASCADE)
1021
1022
1023class UserProxy(User):
1024    """Proxy a model with a different app_label."""
1025
1026    class Meta:
1027        proxy = True

tests/admin_views/admin.py

   1import datetime
   2import os
   3import tempfile
   4from io import StringIO
   5from wsgiref.util import FileWrapper
   6
   7from django import forms
   8from django.contrib import admin
   9from django.contrib.admin import BooleanFieldListFilter
  10from django.contrib.admin.views.main import ChangeList
  11from django.contrib.auth.admin import GroupAdmin, UserAdmin
  12from django.contrib.auth.models import Group, User
  13from django.core.exceptions import ValidationError
  14from django.core.files.storage import FileSystemStorage
  15from django.core.mail import EmailMessage
  16from django.db import models
  17from django.forms.models import BaseModelFormSet
  18from django.http import HttpResponse, StreamingHttpResponse
  19from django.urls import path
  20from django.utils.html import format_html
  21from django.utils.safestring import mark_safe
  22
  23from .forms import MediaActionForm
  24from .models import (
  25    Actor,
  26    AdminOrderedAdminMethod,
  27    AdminOrderedCallable,
  28    AdminOrderedField,
  29    AdminOrderedModelMethod,
  30    Album,
  31    Answer,
  32    Answer2,
  33    Article,
  34    BarAccount,
  35    Book,
  36    Bookmark,
  37    Category,
  38    Chapter,
  39    ChapterXtra1,
  40    Child,
  41    ChildOfReferer,
  42    Choice,
  43    City,
  44    Collector,
  45    Color,
  46    Color2,
  47    ComplexSortedPerson,
  48    CoverLetter,
  49    CustomArticle,
  50    CyclicOne,
  51    CyclicTwo,
  52    DependentChild,
  53    DooHickey,
  54    EmptyModel,
  55    EmptyModelHidden,
  56    EmptyModelMixin,
  57    EmptyModelVisible,
  58    ExplicitlyProvidedPK,
  59    ExternalSubscriber,
  60    Fabric,
  61    FancyDoodad,
  62    FieldOverridePost,
  63    FilteredManager,
  64    FooAccount,
  65    FoodDelivery,
  66    FunkyTag,
  67    Gadget,
  68    Gallery,
  69    GenRelReference,
  70    Grommet,
  71    ImplicitlyGeneratedPK,
  72    Ingredient,
  73    InlineReference,
  74    InlineReferer,
  75    Inquisition,
  76    Language,
  77    Link,
  78    MainPrepopulated,
  79    ModelWithStringPrimaryKey,
  80    NotReferenced,
  81    OldSubscriber,
  82    OtherStory,
  83    Paper,
  84    Parent,
  85    ParentWithDependentChildren,
  86    ParentWithUUIDPK,
  87    Person,
  88    Persona,
  89    Picture,
  90    Pizza,
  91    Plot,
  92    PlotDetails,
  93    PlotProxy,
  94    PluggableSearchPerson,
  95    Podcast,
  96    Post,
  97    PrePopulatedPost,
  98    PrePopulatedPostLargeSlug,
  99    PrePopulatedSubPost,
 100    Promo,
 101    Question,
 102    ReadablePizza,
 103    ReadOnlyPizza,
 104    Recipe,
 105    Recommendation,
 106    Recommender,
 107    ReferencedByGenRel,
 108    ReferencedByInline,
 109    ReferencedByParent,
 110    RelatedPrepopulated,
 111    RelatedWithUUIDPKModel,
 112    Report,
 113    Reservation,
 114    Restaurant,
 115    RowLevelChangePermissionModel,
 116    Section,
 117    ShortMessage,
 118    Simple,
 119    Sketch,
 120    Song,
 121    State,
 122    Story,
 123    StumpJoke,
 124    Subscriber,
 125    SuperVillain,
 126    Telegram,
 127    Thing,
 128    Topping,
 129    UnchangeableObject,
 130    UndeletableObject,
 131    UnorderedObject,
 132    UserMessenger,
 133    UserProxy,
 134    Villain,
 135    Vodcast,
 136    Whatsit,
 137    Widget,
 138    Worker,
 139    WorkHour,
 140)
 141
 142
 143def callable_year(dt_value):
 144    try:
 145        return dt_value.year
 146    except AttributeError:
 147        return None
 148
 149
 150callable_year.admin_order_field = "date"
 151
 152
 153class ArticleInline(admin.TabularInline):
 154    model = Article
 155    fk_name = "section"
 156    prepopulated_fields = {"title": ("content",)}
 157    fieldsets = (
 158        ("Some fields", {"classes": ("collapse",), "fields": ("title", "content")}),
 159        ("Some other fields", {"classes": ("wide",), "fields": ("date", "section")}),
 160    )
 161
 162
 163class ChapterInline(admin.TabularInline):
 164    model = Chapter
 165
 166
 167class ChapterXtra1Admin(admin.ModelAdmin):
 168    list_filter = (
 169        "chap",
 170        "chap__title",
 171        "chap__book",
 172        "chap__book__name",
 173        "chap__book__promo",
 174        "chap__book__promo__name",
 175        "guest_author__promo__book",
 176    )
 177
 178
 179class ArticleForm(forms.ModelForm):
 180    extra_form_field = forms.BooleanField(required=False)
 181
 182    class Meta:
 183        fields = "__all__"
 184        model = Article
 185
 186
 187class ArticleAdmin(admin.ModelAdmin):
 188    list_display = (
 189        "content",
 190        "date",
 191        callable_year,
 192        "model_year",
 193        "modeladmin_year",
 194        "model_year_reversed",
 195        "section",
 196        lambda obj: obj.title,
 197        "order_by_expression",
 198        "model_property_year",
 199        "model_month",
 200        "order_by_f_expression",
 201        "order_by_orderby_expression",
 202    )
 203    list_editable = ("section",)
 204    list_filter = ("date", "section")
 205    autocomplete_fields = ("section",)
 206    view_on_site = False
 207    form = ArticleForm
 208    fieldsets = (
 209        (
 210            "Some fields",
 211            {
 212                "classes": ("collapse",),
 213                "fields": ("title", "content", "extra_form_field"),
 214            },
 215        ),
 216        (
 217            "Some other fields",
 218            {"classes": ("wide",), "fields": ("date", "section", "sub_section")},
 219        ),
 220    )
 221
 222    # These orderings aren't particularly useful but show that expressions can
 223    # be used for admin_order_field.
 224    def order_by_expression(self, obj):
 225        return obj.model_year
 226
 227    order_by_expression.admin_order_field = models.F("date") + datetime.timedelta(
 228        days=3
 229    )
 230
 231    def order_by_f_expression(self, obj):
 232        return obj.model_year
 233
 234    order_by_f_expression.admin_order_field = models.F("date")
 235
 236    def order_by_orderby_expression(self, obj):
 237        return obj.model_year
 238
 239    order_by_orderby_expression.admin_order_field = models.F("date").asc(
 240        nulls_last=True
 241    )
 242
 243    def changelist_view(self, request):
 244        return super().changelist_view(request, extra_context={"extra_var": "Hello!"})
 245
 246    def modeladmin_year(self, obj):
 247        return obj.date.year
 248
 249    modeladmin_year.admin_order_field = "date"
 250    modeladmin_year.short_description = None
 251
 252    def delete_model(self, request, obj):
 253        EmailMessage(
 254            "Greetings from a deleted object",
 255            "I hereby inform you that some user deleted me",
 256            "from@example.com",
 257            ["to@example.com"],
 258        ).send()
 259        return super().delete_model(request, obj)
 260
 261    def save_model(self, request, obj, form, change=True):
 262        EmailMessage(
 263            "Greetings from a created object",
 264            "I hereby inform you that some user created me",
 265            "from@example.com",
 266            ["to@example.com"],
 267        ).send()
 268        return super().save_model(request, obj, form, change)
 269
 270
 271class ArticleAdmin2(admin.ModelAdmin):
 272    def has_module_permission(self, request):
 273        return False
 274
 275
 276class RowLevelChangePermissionModelAdmin(admin.ModelAdmin):
 277    def has_change_permission(self, request, obj=None):
 278        """ Only allow changing objects with even id number """
 279        return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0)
 280
 281    def has_view_permission(self, request, obj=None):
 282        """Only allow viewing objects if id is a multiple of 3."""
 283        return request.user.is_staff and obj is not None and obj.id % 3 == 0
 284
 285
 286class CustomArticleAdmin(admin.ModelAdmin):
 287    """
 288    Tests various hooks for using custom templates and contexts.
 289    """
 290
 291    change_list_template = "custom_admin/change_list.html"
 292    change_form_template = "custom_admin/change_form.html"
 293    add_form_template = "custom_admin/add_form.html"
 294    object_history_template = "custom_admin/object_history.html"
 295    delete_confirmation_template = "custom_admin/delete_confirmation.html"
 296    delete_selected_confirmation_template = (
 297        "custom_admin/delete_selected_confirmation.html"
 298    )
 299    popup_response_template = "custom_admin/popup_response.html"
 300
 301    def changelist_view(self, request):
 302        return super().changelist_view(request, extra_context={"extra_var": "Hello!"})
 303
 304
 305class ThingAdmin(admin.ModelAdmin):
 306    list_filter = ("color", "color__warm", "color__value", "pub_date")
 307
 308
 309class InquisitionAdmin(admin.ModelAdmin):
 310    list_display = ("leader", "country", "expected", "sketch")
 311
 312    def sketch(self, obj):
 313        # A method with the same name as a reverse accessor.
 314        return "list-display-sketch"
 315
 316
 317class SketchAdmin(admin.ModelAdmin):
 318    raw_id_fields = ("inquisition", "defendant0", "defendant1")
 319
 320
 321class FabricAdmin(admin.ModelAdmin):
 322    list_display = ("surface",)
 323    list_filter = ("surface",)
 324
 325
 326class BasePersonModelFormSet(BaseModelFormSet):
 327    def clean(self):
 328        for person_dict in self.cleaned_data:
 329            person = person_dict.get("id")
 330            alive = person_dict.get("alive")
 331            if person and alive and person.name == "Grace Hopper":
 332                raise forms.ValidationError("Grace is not a Zombie")
 333
 334
 335class PersonAdmin(admin.ModelAdmin):
 336    list_display = ("name", "gender", "alive")
 337    list_editable = ("gender", "alive")
 338    list_filter = ("gender",)
 339    search_fields = ("^name",)
 340    save_as = True
 341
 342    def get_changelist_formset(self, request, **kwargs):
 343        return super().get_changelist_formset(
 344            request, formset=BasePersonModelFormSet, **kwargs
 345        )
 346
 347    def get_queryset(self, request):
 348        # Order by a field that isn't in list display, to be able to test
 349        # whether ordering is preserved.
 350        return super().get_queryset(request).order_by("age")
 351
 352
 353class FooAccountAdmin(admin.StackedInline):
 354    model = FooAccount
 355    extra = 1
 356
 357
 358class BarAccountAdmin(admin.StackedInline):
 359    model = BarAccount
 360    extra = 1
 361
 362
 363class PersonaAdmin(admin.ModelAdmin):
 364    inlines = (FooAccountAdmin, BarAccountAdmin)
 365
 366
 367class SubscriberAdmin(admin.ModelAdmin):
 368    actions = ["mail_admin"]
 369    action_form = MediaActionForm
 370
 371    def delete_queryset(self, request, queryset):
 372        SubscriberAdmin.overridden = True
 373        super().delete_queryset(request, queryset)
 374
 375    def mail_admin(self, request, selected):
 376        EmailMessage(
 377            "Greetings from a ModelAdmin action",
 378            "This is the test email from an admin action",
 379            "from@example.com",
 380            ["to@example.com"],
 381        ).send()
 382
 383
 384def external_mail(modeladmin, request, selected):
 385    EmailMessage(
 386        "Greetings from a function action",
 387        "This is the test email from a function action",
 388        "from@example.com",
 389        ["to@example.com"],
 390    ).send()
 391
 392
 393external_mail.short_description = "External mail (Another awesome action)"
 394
 395
 396def redirect_to(modeladmin, request, selected):
 397    from django.http import HttpResponseRedirect
 398
 399    return HttpResponseRedirect("/some-where-else/")
 400
 401
 402redirect_to.short_description = "Redirect to (Awesome action)"
 403
 404
 405def download(modeladmin, request, selected):
 406    buf = StringIO("This is the content of the file")
 407    return StreamingHttpResponse(FileWrapper(buf))
 408
 409
 410download.short_description = "Download subscription"
 411
 412
 413def no_perm(modeladmin, request, selected):
 414    return HttpResponse(content="No permission to perform this action", status=403)
 415
 416
 417no_perm.short_description = "No permission to run"
 418
 419
 420class ExternalSubscriberAdmin(admin.ModelAdmin):
 421    actions = [redirect_to, external_mail, download, no_perm]
 422
 423
 424class PodcastAdmin(admin.ModelAdmin):
 425    list_display = ("name", "release_date")
 426    list_editable = ("release_date",)
 427    date_hierarchy = "release_date"
 428    ordering = ("name",)
 429
 430
 431class VodcastAdmin(admin.ModelAdmin):
 432    list_display = ("name", "released")
 433    list_editable = ("released",)
 434
 435    ordering = ("name",)
 436
 437
 438class ChildInline(admin.StackedInline):
 439    model = Child
 440
 441
 442class ParentAdmin(admin.ModelAdmin):
 443    model = Parent
 444    inlines = [ChildInline]
 445    save_as = True
 446    list_display = (
 447        "id",
 448        "name",
 449    )
 450    list_display_links = ("id",)
 451    list_editable = ("name",)
 452
 453    def save_related(self, request, form, formsets, change):
 454        super().save_related(request, form, formsets, change)
 455        first_name, last_name = form.instance.name.split()
 456        for child in form.instance.child_set.all():
 457            if len(child.name.split()) < 2:
 458                child.name = child.name + " " + last_name
 459                child.save()
 460
 461
 462class EmptyModelAdmin(admin.ModelAdmin):
 463    def get_queryset(self, request):
 464        return super().get_queryset(request).filter(pk__gt=1)
 465
 466
 467class OldSubscriberAdmin(admin.ModelAdmin):
 468    actions = None
 469
 470
 471temp_storage = FileSystemStorage(tempfile.mkdtemp())
 472UPLOAD_TO = os.path.join(temp_storage.location, "test_upload")
 473
 474
 475class PictureInline(admin.TabularInline):
 476    model = Picture
 477    extra = 1
 478
 479
 480class GalleryAdmin(admin.ModelAdmin):
 481    inlines = [PictureInline]
 482
 483
 484class PictureAdmin(admin.ModelAdmin):
 485    pass
 486
 487
 488class LanguageAdmin(admin.ModelAdmin):
 489    list_display = ["iso", "shortlist", "english_name", "name"]
 490    list_editable = ["shortlist"]
 491
 492
 493class RecommendationAdmin(admin.ModelAdmin):
 494    show_full_result_count = False
 495    search_fields = (
 496        "=titletranslation__text",
 497        "=the_recommender__titletranslation__text",
 498    )
 499
 500
 501class WidgetInline(admin.StackedInline):
 502    model = Widget
 503
 504
 505class DooHickeyInline(admin.StackedInline):
 506    model = DooHickey
 507
 508
 509class GrommetInline(admin.StackedInline):
 510    model = Grommet
 511
 512
 513class WhatsitInline(admin.StackedInline):
 514    model = Whatsit
 515
 516
 517class FancyDoodadInline(admin.StackedInline):
 518    model = FancyDoodad
 519
 520
 521class CategoryAdmin(admin.ModelAdmin):
 522    list_display = ("id", "collector", "order")
 523    list_editable = ("order",)
 524
 525
 526class CategoryInline(admin.StackedInline):
 527    model = Category
 528
 529
 530class CollectorAdmin(admin.ModelAdmin):
 531    inlines = [
 532        WidgetInline,
 533        DooHickeyInline,
 534        GrommetInline,
 535        WhatsitInline,
 536        FancyDoodadInline,
 537        CategoryInline,
 538    ]
 539
 540
 541class LinkInline(admin.TabularInline):
 542    model = Link
 543    extra = 1
 544
 545    readonly_fields = ("posted", "multiline", "readonly_link_content")
 546
 547    def multiline(self, instance):
 548        return "InlineMultiline\ntest\nstring"
 549
 550
 551class SubPostInline(admin.TabularInline):
 552    model = PrePopulatedSubPost
 553
 554    prepopulated_fields = {"subslug": ("subtitle",)}
 555
 556    def get_readonly_fields(self, request, obj=None):
 557        if obj and obj.published:
 558            return ("subslug",)
 559        return self.readonly_fields
 560
 561    def get_prepopulated_fields(self, request, obj=None):
 562        if obj and obj.published:
 563            return {}
 564        return self.prepopulated_fields
 565
 566
 567class PrePopulatedPostAdmin(admin.ModelAdmin):
 568    list_display = ["title", "slug"]
 569    prepopulated_fields = {"slug": ("title",)}
 570
 571    inlines = [SubPostInline]
 572
 573    def get_readonly_fields(self, request, obj=None):
 574        if obj and obj.published:
 575            return ("slug",)
 576        return self.readonly_fields
 577
 578    def get_prepopulated_fields(self, request, obj=None):
 579        if obj and obj.published:
 580            return {}
 581        return self.prepopulated_fields
 582
 583
 584class PrePopulatedPostReadOnlyAdmin(admin.ModelAdmin):
 585    prepopulated_fields = {"slug": ("title",)}
 586
 587    def has_change_permission(self, *args, **kwargs):
 588        return False
 589
 590
 591class PostAdmin(admin.ModelAdmin):
 592    list_display = ["title", "public"]
 593    readonly_fields = (
 594        "posted",
 595        "awesomeness_level",
 596        "coolness",
 597        "value",
 598        "multiline",
 599        "multiline_html",
 600        lambda obj: "foo",
 601        "readonly_content",
 602    )
 603
 604    inlines = [LinkInline]
 605
 606    def coolness(self, instance):
 607        if instance.pk:
 608            return "%d amount of cool." % instance.pk
 609        else:
 610            return "Unknown coolness."
 611
 612    def value(self, instance):
 613        return 1000
 614
 615    value.short_description = "Value in $US"
 616
 617    def multiline(self, instance):
 618        return "Multiline\ntest\nstring"
 619
 620    def multiline_html(self, instance):
 621        return mark_safe("Multiline<br>\nhtml<br>\ncontent")
 622
 623
 624class FieldOverridePostForm(forms.ModelForm):
 625    model = FieldOverridePost
 626
 627    class Meta:
 628        help_texts = {
 629            "posted": "Overridden help text for the date",
 630        }
 631        labels = {
 632            "public": "Overridden public label",
 633        }
 634
 635
 636class FieldOverridePostAdmin(PostAdmin):
 637    form = FieldOverridePostForm
 638
 639
 640class CustomChangeList(ChangeList):
 641    def get_queryset(self, request):
 642        return self.root_queryset.order_by("pk").filter(pk=9999)  # Doesn't exist
 643
 644
 645class GadgetAdmin(admin.ModelAdmin):
 646    def get_changelist(self, request, **kwargs):
 647        return CustomChangeList
 648
 649
 650class ToppingAdmin(admin.ModelAdmin):
 651    readonly_fields = ("pizzas",)
 652
 653
 654class PizzaAdmin(admin.ModelAdmin):
 655    readonly_fields = ("toppings",)
 656
 657
 658class StudentAdmin(admin.ModelAdmin):
 659    search_fields = ("name",)
 660
 661
 662class ReadOnlyPizzaAdmin(admin.ModelAdmin):
 663    readonly_fields = ("name", "toppings")
 664
 665    def has_add_permission(self, request):
 666        return False
 667
 668    def has_change_permission(self, request, obj=None):
 669        return True
 670
 671    def has_delete_permission(self, request, obj=None):
 672        return True
 673
 674
 675class WorkHourAdmin(admin.ModelAdmin):
 676    list_display = ("datum", "employee")
 677    list_filter = ("employee",)
 678
 679
 680class FoodDeliveryAdmin(admin.ModelAdmin):
 681    list_display = ("reference", "driver", "restaurant")
 682    list_editable = ("driver", "restaurant")
 683
 684
 685class CoverLetterAdmin(admin.ModelAdmin):
 686    """
 687    A ModelAdmin with a custom get_queryset() method that uses defer(), to test
 688    verbose_name display in messages shown after adding/editing CoverLetter
 689    instances. Note that the CoverLetter model defines a __str__ method.
 690    For testing fix for ticket #14529.
 691    """
 692
 693    def get_queryset(self, request):
 694        return super().get_queryset(request).defer("date_written")
 695
 696
 697class PaperAdmin(admin.ModelAdmin):
 698    """
 699    A ModelAdmin with a custom get_queryset() method that uses only(), to test
 700    verbose_name display in messages shown after adding/editing Paper
 701    instances.
 702    For testing fix for ticket #14529.
 703    """
 704
 705    def get_queryset(self, request):
 706        return super().get_queryset(request).only("title")
 707
 708
 709class ShortMessageAdmin(admin.ModelAdmin):
 710    """
 711    A ModelAdmin with a custom get_queryset() method that uses defer(), to test
 712    verbose_name display in messages shown after adding/editing ShortMessage
 713    instances.
 714    For testing fix for ticket #14529.
 715    """
 716
 717    def get_queryset(self, request):
 718        return super().get_queryset(request).defer("timestamp")
 719
 720
 721class TelegramAdmin(admin.ModelAdmin):
 722    """
 723    A ModelAdmin with a custom get_queryset() method that uses only(), to test
 724    verbose_name display in messages shown after adding/editing Telegram
 725    instances. Note that the Telegram model defines a __str__ method.
 726    For testing fix for ticket #14529.
 727    """
 728
 729    def get_queryset(self, request):
 730        return super().get_queryset(request).only("title")
 731
 732
 733class StoryForm(forms.ModelForm):
 734    class Meta:
 735        widgets = {"title": forms.HiddenInput}
 736
 737
 738class StoryAdmin(admin.ModelAdmin):
 739    list_display = ("id", "title", "content")
 740    list_display_links = ("title",)  # 'id' not in list_display_links
 741    list_editable = ("content",)
 742    form = StoryForm
 743    ordering = ["-id"]
 744
 745
 746class OtherStoryAdmin(admin.ModelAdmin):
 747    list_display = ("id", "title", "content")
 748    list_display_links = ("title", "id")  # 'id' in list_display_links
 749    list_editable = ("content",)
 750    ordering = ["-id"]
 751
 752
 753class ComplexSortedPersonAdmin(admin.ModelAdmin):
 754    list_display = ("name", "age", "is_employee", "colored_name")
 755    ordering = ("name",)
 756
 757    def colored_name(self, obj):
 758        return format_html('<span style="color: #ff00ff;">{}</span>', obj.name)
 759
 760    colored_name.admin_order_field = "name"
 761
 762
 763class PluggableSearchPersonAdmin(admin.ModelAdmin):
 764    list_display = ("name", "age")
 765    search_fields = ("name",)
 766
 767    def get_search_results(self, request, queryset, search_term):
 768        queryset, use_distinct = super().get_search_results(
 769            request, queryset, search_term
 770        )
 771        try:
 772            search_term_as_int = int(search_term)
 773        except ValueError:
 774            pass
 775        else:
 776            queryset |= self.model.objects.filter(age=search_term_as_int)
 777        return queryset, use_distinct
 778
 779
 780class AlbumAdmin(admin.ModelAdmin):
 781    list_filter = ["title"]
 782
 783
 784class QuestionAdmin(admin.ModelAdmin):
 785    ordering = ["-posted"]
 786    search_fields = ["question"]
 787    autocomplete_fields = ["related_questions"]
 788
 789
 790class AnswerAdmin(admin.ModelAdmin):
 791    autocomplete_fields = ["question"]
 792
 793
 794class PrePopulatedPostLargeSlugAdmin(admin.ModelAdmin):
 795    prepopulated_fields = {"slug": ("title",)}
 796
 797
 798class AdminOrderedFieldAdmin(admin.ModelAdmin):
 799    ordering = ("order",)
 800    list_display = ("stuff", "order")
 801
 802
 803class AdminOrderedModelMethodAdmin(admin.ModelAdmin):
 804    ordering = ("order",)
 805    list_display = ("stuff", "some_order")
 806
 807
 808class AdminOrderedAdminMethodAdmin(admin.ModelAdmin):
 809    def some_admin_order(self, obj):
 810        return obj.order
 811
 812    some_admin_order.admin_order_field = "order"
 813    ordering = ("order",)
 814    list_display = ("stuff", "some_admin_order")
 815
 816
 817def admin_ordered_callable(obj):
 818    return obj.order
 819
 820
 821admin_ordered_callable.admin_order_field = "order"
 822
 823
 824class AdminOrderedCallableAdmin(admin.ModelAdmin):
 825    ordering = ("order",)
 826    list_display = ("stuff", admin_ordered_callable)
 827
 828
 829class ReportAdmin(admin.ModelAdmin):
 830    def extra(self, request):
 831        return HttpResponse()
 832
 833    def get_urls(self):
 834        # Corner case: Don't call parent implementation
 835        return [path("extra/", self.extra, name="cable_extra")]
 836
 837
 838class CustomTemplateBooleanFieldListFilter(BooleanFieldListFilter):
 839    template = "custom_filter_template.html"
 840
 841
 842class CustomTemplateFilterColorAdmin(admin.ModelAdmin):
 843    list_filter = (("warm", CustomTemplateBooleanFieldListFilter),)
 844
 845
 846# For Selenium Prepopulated tests -------------------------------------
 847class RelatedPrepopulatedInline1(admin.StackedInline):
 848    fieldsets = (
 849        (
 850            None,
 851            {
 852                "fields": (
 853                    ("fk", "m2m"),
 854                    ("pubdate", "status"),
 855                    ("name", "slug1", "slug2",),
 856                ),
 857            },
 858        ),
 859    )
 860    formfield_overrides = {models.CharField: {"strip": False}}
 861    model = RelatedPrepopulated
 862    extra = 1
 863    autocomplete_fields = ["fk", "m2m"]
 864    prepopulated_fields = {
 865        "slug1": ["name", "pubdate"],
 866        "slug2": ["status", "name"],
 867    }
 868
 869
 870class RelatedPrepopulatedInline2(admin.TabularInline):
 871    model = RelatedPrepopulated
 872    extra = 1
 873    autocomplete_fields = ["fk", "m2m"]
 874    prepopulated_fields = {
 875        "slug1": ["name", "pubdate"],
 876        "slug2": ["status", "name"],
 877    }
 878
 879
 880class RelatedPrepopulatedInline3(admin.TabularInline):
 881    model = RelatedPrepopulated
 882    extra = 0
 883    autocomplete_fields = ["fk", "m2m"]
 884
 885
 886class MainPrepopulatedAdmin(admin.ModelAdmin):
 887    inlines = [
 888        RelatedPrepopulatedInline1,
 889        RelatedPrepopulatedInline2,
 890        RelatedPrepopulatedInline3,
 891    ]
 892    fieldsets = (
 893        (
 894            None,
 895            {"fields": (("pubdate", "status"), ("name", "slug1", "slug2", "slug3"))},
 896        ),
 897    )
 898    formfield_overrides = {models.CharField: {"strip": False}}
 899    prepopulated_fields = {
 900        "slug1": ["name", "pubdate"],
 901        "slug2": ["status", "name"],
 902        "slug3": ["name"],
 903    }
 904
 905
 906class UnorderedObjectAdmin(admin.ModelAdmin):
 907    list_display = ["id", "name"]
 908    list_display_links = ["id"]
 909    list_editable = ["name"]
 910    list_per_page = 2
 911
 912
 913class UndeletableObjectAdmin(admin.ModelAdmin):
 914    def change_view(self, *args, **kwargs):
 915        kwargs["extra_context"] = {"show_delete": False}
 916        return super().change_view(*args, **kwargs)
 917
 918
 919class UnchangeableObjectAdmin(admin.ModelAdmin):
 920    def get_urls(self):
 921        # Disable change_view, but leave other urls untouched
 922        urlpatterns = super().get_urls()
 923        return [p for p in urlpatterns if p.name and not p.name.endswith("_change")]
 924
 925
 926def callable_on_unknown(obj):
 927    return obj.unknown
 928
 929
 930class AttributeErrorRaisingAdmin(admin.ModelAdmin):
 931    list_display = [callable_on_unknown]
 932
 933
 934class CustomManagerAdmin(admin.ModelAdmin):
 935    def get_queryset(self, request):
 936        return FilteredManager.objects
 937
 938
 939class MessageTestingAdmin(admin.ModelAdmin):
 940    actions = [
 941        "message_debug",
 942        "message_info",
 943        "message_success",
 944        "message_warning",
 945        "message_error",
 946        "message_extra_tags",
 947    ]
 948
 949    def message_debug(self, request, selected):
 950        self.message_user(request, "Test debug", level="debug")
 951
 952    def message_info(self, request, selected):
 953        self.message_user(request, "Test info", level="info")
 954
 955    def message_success(self, request, selected):
 956        self.message_user(request, "Test success", level="success")
 957
 958    def message_warning(self, request, selected):
 959        self.message_user(request, "Test warning", level="warning")
 960
 961    def message_error(self, request, selected):
 962        self.message_user(request, "Test error", level="error")
 963
 964    def message_extra_tags(self, request, selected):
 965        self.message_user(request, "Test tags", extra_tags="extra_tag")
 966
 967
 968class ChoiceList(admin.ModelAdmin):
 969    list_display = ["choice"]
 970    readonly_fields = ["choice"]
 971    fields = ["choice"]
 972
 973
 974class DependentChildAdminForm(forms.ModelForm):
 975    """
 976    Issue #20522
 977    Form to test child dependency on parent object's validation
 978    """
 979
 980    def clean(self):
 981        parent = self.cleaned_data.get("parent")
 982        if parent.family_name and parent.family_name != self.cleaned_data.get(
 983            "family_name"
 984        ):
 985            raise ValidationError(
 986                "Children must share a family name with their parents "
 987                + "in this contrived test case"
 988            )
 989        return super().clean()
 990
 991
 992class DependentChildInline(admin.TabularInline):
 993    model = DependentChild
 994    form = DependentChildAdminForm
 995
 996
 997class ParentWithDependentChildrenAdmin(admin.ModelAdmin):
 998    inlines = [DependentChildInline]
 999
1000
1001# Tests for ticket 11277 ----------------------------------
1002
1003
1004class FormWithoutHiddenField(forms.ModelForm):
1005    first = forms.CharField()
1006    second = forms.CharField()
1007
1008
1009class FormWithoutVisibleField(forms.ModelForm):
1010    first = forms.CharField(widget=forms.HiddenInput)
1011    second = forms.CharField(widget=forms.HiddenInput)
1012
1013
1014class FormWithVisibleAndHiddenField(forms.ModelForm):
1015    first = forms.CharField(widget=forms.HiddenInput)
1016    second = forms.CharField()
1017
1018
1019class EmptyModelVisibleAdmin(admin.ModelAdmin):
1020    form = FormWithoutHiddenField
1021    fieldsets = ((None, {"fields": (("first", "second"),),}),)
1022
1023
1024class EmptyModelHiddenAdmin(admin.ModelAdmin):
1025    form = FormWithoutVisibleField
1026    fieldsets = EmptyModelVisibleAdmin.fieldsets
1027
1028
1029class EmptyModelMixinAdmin(admin.ModelAdmin):
1030    form = FormWithVisibleAndHiddenField
1031    fieldsets = EmptyModelVisibleAdmin.fieldsets
1032
1033
1034class CityInlineAdmin(admin.TabularInline):
1035    model = City
1036    view_on_site = False
1037
1038
1039class StateAdminForm(forms.ModelForm):
1040    nolabel_form_field = forms.BooleanField(required=False)
1041
1042    class Meta:
1043        model = State
1044        fields = "__all__"
1045        labels = {"name": "State name (from form’s Meta.labels)"}
1046
1047    @property
1048    def changed_data(self):
1049        data = super().changed_data
1050        if data:
1051            # Add arbitrary name to changed_data to test
1052            # change message construction.
1053            return data + ["not_a_form_field"]
1054        return data
1055
1056
1057class StateAdmin(admin.ModelAdmin):
1058    inlines = [CityInlineAdmin]
1059    form = StateAdminForm
1060
1061
1062class RestaurantInlineAdmin(admin.TabularInline):
1063    model = Restaurant
1064    view_on_site = True
1065
1066
1067class CityAdmin(admin.ModelAdmin):
1068    inlines = [RestaurantInlineAdmin]
1069    view_on_site = True
1070
1071
1072class WorkerAdmin(admin.ModelAdmin):
1073    def view_on_site(self, obj):
1074        return "/worker/%s/%s/" % (obj.surname, obj.name)
1075
1076
1077class WorkerInlineAdmin(admin.TabularInline):
1078    model = Worker
1079
1080    def view_on_site(self, obj):
1081        return "/worker_inline/%s/%s/" % (obj.surname, obj.name)
1082
1083
1084class RestaurantAdmin(admin.ModelAdmin):
1085    inlines = [WorkerInlineAdmin]
1086    view_on_site = False
1087
1088    def get_changeform_initial_data(self, request):
1089        return {"name": "overridden_value"}
1090
1091
1092class FunkyTagAdmin(admin.ModelAdmin):
1093    list_display = ("name", "content_object")
1094
1095
1096class InlineReferenceInline(admin.TabularInline):
1097    model = InlineReference
1098
1099
1100class InlineRefererAdmin(admin.ModelAdmin):
1101    inlines = [InlineReferenceInline]
1102
1103
1104class PlotReadonlyAdmin(admin.ModelAdmin):
1105    readonly_fields = ("plotdetails",)
1106
1107
1108class GetFormsetsArgumentCheckingAdmin(admin.ModelAdmin):
1109    fields = ["name"]
1110
1111    def add_view(self, request, *args, **kwargs):
1112        request.is_add_view = True
1113        return super().add_view(request, *args, **kwargs)
1114
1115    def change_view(self, request, *args, **kwargs):
1116        request.is_add_view = False
1117        return super().change_view(request, *args, **kwargs)
1118
1119    def get_formsets_with_inlines(self, request, obj=None):
1120        if request.is_add_view and obj is not None:
1121            raise Exception(
1122                "'obj' passed to get_formsets_with_inlines wasn't None during add_view"
1123            )
1124        if not request.is_add_view and obj is None:
1125            raise Exception(
1126                "'obj' passed to get_formsets_with_inlines was None during change_view"
1127            )
1128        return super().get_formsets_with_inlines(request, obj)
1129
1130
1131site = admin.AdminSite(name="admin")
1132site.site_url = "/my-site-url/"
1133site.register(Article, ArticleAdmin)
1134site.register(CustomArticle, CustomArticleAdmin)
1135site.register(
1136    Section,
1137    save_as=True,
1138    inlines=[ArticleInline],
1139    readonly_fields=["name_property"],
1140    search_fields=["name"],
1141)
1142site.register(ModelWithStringPrimaryKey)
1143site.register(Color)
1144site.register(Thing, ThingAdmin)
1145site.register(Actor)
1146site.register(Inquisition, InquisitionAdmin)
1147site.register(Sketch, SketchAdmin)
1148site.register(Person, PersonAdmin)
1149site.register(Persona, PersonaAdmin)
1150site.register(Subscriber, SubscriberAdmin)
1151site.register(ExternalSubscriber, ExternalSubscriberAdmin)
1152site.register(OldSubscriber, OldSubscriberAdmin)
1153site.register(Podcast, PodcastAdmin)
1154site.register(Vodcast, VodcastAdmin)
1155site.register(Parent, ParentAdmin)
1156site.register(EmptyModel, EmptyModelAdmin)
1157site.register(Fabric, FabricAdmin)
1158site.register(Gallery, GalleryAdmin)
1159site.register(Picture, PictureAdmin)
1160site.register(Language, LanguageAdmin)
1161site.register(Recommendation, RecommendationAdmin)
1162site.register(Recommender)
1163site.register(Collector, CollectorAdmin)
1164site.register(Category, CategoryAdmin)
1165site.register(Post, PostAdmin)
1166site.register(FieldOverridePost, FieldOverridePostAdmin)
1167site.register(Gadget, GadgetAdmin)
1168site.register(Villain)
1169site.register(SuperVillain)
1170site.register(Plot)
1171site.register(PlotDetails)
1172site.register(PlotProxy, PlotReadonlyAdmin)
1173site.register(Bookmark)
1174site.register(CyclicOne)
1175site.register(CyclicTwo)
1176site.register(WorkHour, WorkHourAdmin)
1177site.register(Reservation)
1178site.register(FoodDelivery, FoodDeliveryAdmin)
1179site.register(RowLevelChangePermissionModel, RowLevelChangePermissionModelAdmin)
1180site.register(Paper, PaperAdmin)
1181site.register(CoverLetter, CoverLetterAdmin)
1182site.register(ShortMessage, ShortMessageAdmin)
1183site.register(Telegram, TelegramAdmin)
1184site.register(Story, StoryAdmin)
1185site.register(OtherStory, OtherStoryAdmin)
1186site.register(Report, ReportAdmin)
1187site.register(MainPrepopulated, MainPrepopulatedAdmin)
1188site.register(UnorderedObject, UnorderedObjectAdmin)
1189site.register(UndeletableObject, UndeletableObjectAdmin)
1190site.register(UnchangeableObject, UnchangeableObjectAdmin)
1191site.register(State, StateAdmin)
1192site.register(City, CityAdmin)
1193site.register(Restaurant, RestaurantAdmin)
1194site.register(Worker, WorkerAdmin)
1195site.register(FunkyTag, FunkyTagAdmin)
1196site.register(ReferencedByParent)
1197site.register(ChildOfReferer)
1198site.register(ReferencedByInline)
1199site.register(InlineReferer, InlineRefererAdmin)
1200site.register(ReferencedByGenRel)
1201site.register(GenRelReference)
1202site.register(ParentWithUUIDPK)
1203site.register(RelatedPrepopulated, search_fields=["name"])
1204site.register(RelatedWithUUIDPKModel)
1205
1206# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
1207# That way we cover all four cases:
1208#     related ForeignKey object registered in admin
1209#     related ForeignKey object not registered in admin
1210#     related OneToOne object registered in admin
1211#     related OneToOne object not registered in admin
1212# when deleting Book so as exercise all four paths through
1213# contrib.admin.utils's get_deleted_objects function.
1214site.register(Book, inlines=[ChapterInline])
1215site.register(Promo)
1216site.register(ChapterXtra1, ChapterXtra1Admin)
1217site.register(Pizza, PizzaAdmin)
1218site.register(ReadOnlyPizza, ReadOnlyPizzaAdmin)
1219site.register(ReadablePizza)
1220site.register(Topping, ToppingAdmin)
1221site.register(Album, AlbumAdmin)
1222site.register(Song)
1223site.register(Question, QuestionAdmin)
1224site.register(Answer, AnswerAdmin, date_hierarchy="question__posted")
1225site.register(Answer2, date_hierarchy="question__expires")
1226site.register(PrePopulatedPost, PrePopulatedPostAdmin)
1227site.register(ComplexSortedPerson, ComplexSortedPersonAdmin)
1228site.register(FilteredManager, CustomManagerAdmin)
1229site.register(PluggableSearchPerson, PluggableSearchPersonAdmin)
1230site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin)
1231site.register(AdminOrderedField, AdminOrderedFieldAdmin)
1232site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin)
1233site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin)
1234site.register(AdminOrderedCallable, AdminOrderedCallableAdmin)
1235site.register(Color2, CustomTemplateFilterColorAdmin)
1236site.register(Simple, AttributeErrorRaisingAdmin)
1237site.register(UserMessenger, MessageTestingAdmin)
1238site.register(Choice, ChoiceList)
1239site.register(ParentWithDependentChildren, ParentWithDependentChildrenAdmin)
1240site.register(EmptyModelHidden, EmptyModelHiddenAdmin)
1241site.register(EmptyModelVisible, EmptyModelVisibleAdmin)
1242site.register(EmptyModelMixin, EmptyModelMixinAdmin)
1243site.register(StumpJoke)
1244site.register(Recipe)
1245site.register(Ingredient)
1246site.register(NotReferenced)
1247site.register(ExplicitlyProvidedPK, GetFormsetsArgumentCheckingAdmin)
1248site.register(ImplicitlyGeneratedPK, GetFormsetsArgumentCheckingAdmin)
1249site.register(UserProxy)
1250
1251# Register core models we need in our tests
1252site.register(User, UserAdmin)
1253site.register(Group, GroupAdmin)
1254
1255# Used to test URL namespaces
1256site2 = admin.AdminSite(name="namespaced_admin")
1257site2.register(User, UserAdmin)
1258site2.register(Group, GroupAdmin)
1259site2.register(ParentWithUUIDPK)
1260site2.register(
1261    RelatedWithUUIDPKModel,
1262    list_display=["pk", "parent"],
1263    list_editable=["parent"],
1264    raw_id_fields=["parent"],
1265)
1266site2.register(Person, save_as_continue=False)
1267
1268site7 = admin.AdminSite(name="admin7")
1269site7.register(Article, ArticleAdmin2)
1270site7.register(Section)
1271site7.register(PrePopulatedPost, PrePopulatedPostReadOnlyAdmin)
1272
1273
1274# Used to test ModelAdmin.sortable_by and get_sortable_by().
1275class ArticleAdmin6(admin.ModelAdmin):
1276    list_display = (
1277        "content",
1278        "date",
1279        callable_year,
1280        "model_year",
1281        "modeladmin_year",
1282        "model_year_reversed",
1283        "section",
1284    )
1285    sortable_by = ("date", callable_year)
1286
1287    def modeladmin_year(self, obj):
1288        return obj.date.year
1289
1290    modeladmin_year.admin_order_field = "date"
1291
1292
1293class ActorAdmin6(admin.ModelAdmin):
1294    list_display = ("name", "age")
1295    sortable_by = ("name",)
1296
1297    def get_sortable_by(self, request):
1298        return ("age",)
1299
1300
1301class ChapterAdmin6(admin.ModelAdmin):
1302    list_display = ("title", "book")
1303    sortable_by = ()
1304
1305
1306class ColorAdmin6(admin.ModelAdmin):
1307    list_display = ("value",)
1308
1309    def get_sortable_by(self, request):
1310        return ()
1311
1312
1313site6 = admin.AdminSite(name="admin6")
1314site6.register(Article, ArticleAdmin6)
1315site6.register(Actor, ActorAdmin6)
1316site6.register(Chapter, ChapterAdmin6)
1317site6.register(Color, ColorAdmin6)
1318
1319
1320class ArticleAdmin9(admin.ModelAdmin):
1321    def has_change_permission(self, request, obj=None):
1322        # Simulate that the user can't change a specific object.
1323        return obj is None
1324
1325
1326site9 = admin.AdminSite(name="admin9")
1327site9.register(Article, ArticleAdmin9)

tests/admin_views/tests.py

   1import datetime
   2import os
   3import re
   4import unittest
   5from unittest import mock
   6from urllib.parse import parse_qsl, urljoin, urlparse
   7
   8import pytz
   9
  10from django.contrib.admin import AdminSite, ModelAdmin
  11from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
  12from django.contrib.admin.models import ADDITION, DELETION, LogEntry
  13from django.contrib.admin.options import TO_FIELD_VAR
  14from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
  15from django.contrib.admin.tests import AdminSeleniumTestCase
  16from django.contrib.admin.utils import quote
  17from django.contrib.admin.views.main import IS_POPUP_VAR
  18from django.contrib.auth import REDIRECT_FIELD_NAME, get_permission_codename
  19from django.contrib.auth.models import Group, Permission, User
  20from django.contrib.contenttypes.models import ContentType
  21from django.core import mail
  22from django.core.checks import Error
  23from django.core.files import temp as tempfile
  24from django.forms.utils import ErrorList
  25from django.template.response import TemplateResponse
  26from django.test import (
  27    TestCase,
  28    modify_settings,
  29    override_settings,
  30    skipUnlessDBFeature,
  31)
  32from django.test.utils import override_script_prefix
  33from django.urls import NoReverseMatch, resolve, reverse
  34from django.utils import formats, translation
  35from django.utils.cache import get_max_age
  36from django.utils.encoding import iri_to_uri
  37from django.utils.html import escape
  38from django.utils.http import urlencode
  39
  40from . import customadmin
  41from .admin import CityAdmin, site, site2
  42from .models import (
  43    Actor,
  44    AdminOrderedAdminMethod,
  45    AdminOrderedCallable,
  46    AdminOrderedField,
  47    AdminOrderedModelMethod,
  48    Album,
  49    Answer,
  50    Answer2,
  51    Article,
  52    BarAccount,
  53    Book,
  54    Bookmark,
  55    Category,
  56    Chapter,
  57    ChapterXtra1,
  58    ChapterXtra2,
  59    Character,
  60    Child,
  61    Choice,
  62    City,
  63    Collector,
  64    Color,
  65    ComplexSortedPerson,
  66    CoverLetter,
  67    CustomArticle,
  68    CyclicOne,
  69    CyclicTwo,
  70    DooHickey,
  71    Employee,
  72    EmptyModel,
  73    Fabric,
  74    FancyDoodad,
  75    FieldOverridePost,
  76    FilteredManager,
  77    FooAccount,
  78    FoodDelivery,
  79    FunkyTag,
  80    Gallery,
  81    Grommet,
  82    Inquisition,
  83    Language,
  84    Link,
  85    MainPrepopulated,
  86    Media,
  87    ModelWithStringPrimaryKey,
  88    OtherStory,
  89    Paper,
  90    Parent,
  91    ParentWithDependentChildren,
  92    ParentWithUUIDPK,
  93    Person,
  94    Persona,
  95    Picture,
  96    Pizza,
  97    Plot,
  98    PlotDetails,
  99    PluggableSearchPerson,
 100    Podcast,
 101    Post,
 102    PrePopulatedPost,
 103    Promo,
 104    Question,
 105    ReadablePizza,
 106    ReadOnlyPizza,
 107    Recommendation,
 108    Recommender,
 109    RelatedPrepopulated,
 110    RelatedWithUUIDPKModel,
 111    Report,
 112    Restaurant,
 113    RowLevelChangePermissionModel,
 114    SecretHideout,
 115    Section,
 116    ShortMessage,
 117    Simple,
 118    Song,
 119    State,
 120    Story,
 121    SuperSecretHideout,
 122    SuperVillain,
 123    Telegram,
 124    TitleTranslation,
 125    Topping,
 126    UnchangeableObject,
 127    UndeletableObject,
 128    UnorderedObject,
 129    UserProxy,
 130    Villain,
 131    Vodcast,
 132    Whatsit,
 133    Widget,
 134    Worker,
 135    WorkHour,
 136)
 137
 138ERROR_MESSAGE = "Please enter the correct username and password \
 139for a staff account. Note that both fields may be case-sensitive."
 140
 141MULTIPART_ENCTYPE = 'enctype="multipart/form-data"'
 142
 143
 144class AdminFieldExtractionMixin:
 145    """
 146    Helper methods for extracting data from AdminForm.
 147    """
 148
 149    def get_admin_form_fields(self, response):
 150        """
 151        Return a list of AdminFields for the AdminForm in the response.
 152        """
 153        fields = []
 154        for fieldset in response.context["adminform"]:
 155            for field_line in fieldset:
 156                fields.extend(field_line)
 157        return fields
 158
 159    def get_admin_readonly_fields(self, response):
 160        """
 161        Return the readonly fields for the response's AdminForm.
 162        """
 163        return [f for f in self.get_admin_form_fields(response) if f.is_readonly]
 164
 165    def get_admin_readonly_field(self, response, field_name):
 166        """
 167        Return the readonly field for the given field_name.
 168        """
 169        admin_readonly_fields = self.get_admin_readonly_fields(response)
 170        for field in admin_readonly_fields:
 171            if field.field["name"] == field_name:
 172                return field
 173
 174
 175@override_settings(
 176    ROOT_URLCONF="admin_views.urls", USE_I18N=True, USE_L10N=False, LANGUAGE_CODE="en"
 177)
 178class AdminViewBasicTestCase(TestCase):
 179    @classmethod
 180    def setUpTestData(cls):
 181        cls.superuser = User.objects.create_superuser(
 182            username="super", password="secret", email="super@example.com"
 183        )
 184        cls.s1 = Section.objects.create(name="Test section")
 185        cls.a1 = Article.objects.create(
 186            content="<p>Middle content</p>",
 187            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
 188            section=cls.s1,
 189        )
 190        cls.a2 = Article.objects.create(
 191            content="<p>Oldest content</p>",
 192            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
 193            section=cls.s1,
 194        )
 195        cls.a3 = Article.objects.create(
 196            content="<p>Newest content</p>",
 197            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
 198            section=cls.s1,
 199        )
 200        cls.p1 = PrePopulatedPost.objects.create(
 201            title="A Long Title", published=True, slug="a-long-title"
 202        )
 203        cls.color1 = Color.objects.create(value="Red", warm=True)
 204        cls.color2 = Color.objects.create(value="Orange", warm=True)
 205        cls.color3 = Color.objects.create(value="Blue", warm=False)
 206        cls.color4 = Color.objects.create(value="Green", warm=False)
 207        cls.fab1 = Fabric.objects.create(surface="x")
 208        cls.fab2 = Fabric.objects.create(surface="y")
 209        cls.fab3 = Fabric.objects.create(surface="plain")
 210        cls.b1 = Book.objects.create(name="Book 1")
 211        cls.b2 = Book.objects.create(name="Book 2")
 212        cls.pro1 = Promo.objects.create(name="Promo 1", book=cls.b1)
 213        cls.pro1 = Promo.objects.create(name="Promo 2", book=cls.b2)
 214        cls.chap1 = Chapter.objects.create(
 215            title="Chapter 1", content="[ insert contents here ]", book=cls.b1
 216        )
 217        cls.chap2 = Chapter.objects.create(
 218            title="Chapter 2", content="[ insert contents here ]", book=cls.b1
 219        )
 220        cls.chap3 = Chapter.objects.create(
 221            title="Chapter 1", content="[ insert contents here ]", book=cls.b2
 222        )
 223        cls.chap4 = Chapter.objects.create(
 224            title="Chapter 2", content="[ insert contents here ]", book=cls.b2
 225        )
 226        cls.cx1 = ChapterXtra1.objects.create(chap=cls.chap1, xtra="ChapterXtra1 1")
 227        cls.cx2 = ChapterXtra1.objects.create(chap=cls.chap3, xtra="ChapterXtra1 2")
 228        Actor.objects.create(name="Palin", age=27)
 229
 230        # Post data for edit inline
 231        cls.inline_post_data = {
 232            "name": "Test section",
 233            # inline data
 234            "article_set-TOTAL_FORMS": "6",
 235            "article_set-INITIAL_FORMS": "3",
 236            "article_set-MAX_NUM_FORMS": "0",
 237            "article_set-0-id": cls.a1.pk,
 238            # there is no title in database, give one here or formset will fail.
 239            "article_set-0-title": "Norske bostaver æøå skaper problemer",
 240            "article_set-0-content": "&lt;p&gt;Middle content&lt;/p&gt;",
 241            "article_set-0-date_0": "2008-03-18",
 242            "article_set-0-date_1": "11:54:58",
 243            "article_set-0-section": cls.s1.pk,
 244            "article_set-1-id": cls.a2.pk,
 245            "article_set-1-title": "Need a title.",
 246            "article_set-1-content": "&lt;p&gt;Oldest content&lt;/p&gt;",
 247            "article_set-1-date_0": "2000-03-18",
 248            "article_set-1-date_1": "11:54:58",
 249            "article_set-2-id": cls.a3.pk,
 250            "article_set-2-title": "Need a title.",
 251            "article_set-2-content": "&lt;p&gt;Newest content&lt;/p&gt;",
 252            "article_set-2-date_0": "2009-03-18",
 253            "article_set-2-date_1": "11:54:58",
 254            "article_set-3-id": "",
 255            "article_set-3-title": "",
 256            "article_set-3-content": "",
 257            "article_set-3-date_0": "",
 258            "article_set-3-date_1": "",
 259            "article_set-4-id": "",
 260            "article_set-4-title": "",
 261            "article_set-4-content": "",
 262            "article_set-4-date_0": "",
 263            "article_set-4-date_1": "",
 264            "article_set-5-id": "",
 265            "article_set-5-title": "",
 266            "article_set-5-content": "",
 267            "article_set-5-date_0": "",
 268            "article_set-5-date_1": "",
 269        }
 270
 271    def setUp(self):
 272        self.client.force_login(self.superuser)
 273
 274    def assertContentBefore(self, response, text1, text2, failing_msg=None):
 275        """
 276        Testing utility asserting that text1 appears before text2 in response
 277        content.
 278        """
 279        self.assertEqual(response.status_code, 200)
 280        self.assertLess(
 281            response.content.index(text1.encode()),
 282            response.content.index(text2.encode()),
 283            (failing_msg or "")
 284            + "\nResponse:\n"
 285            + response.content.decode(response.charset),
 286        )
 287
 288
 289class AdminViewBasicTest(AdminViewBasicTestCase):
 290    def test_trailing_slash_required(self):
 291        """
 292        If you leave off the trailing slash, app should redirect and add it.
 293        """
 294        add_url = reverse("admin:admin_views_article_add")
 295        response = self.client.get(add_url[:-1])
 296        self.assertRedirects(response, add_url, status_code=301)
 297
 298    def test_basic_add_GET(self):
 299        """
 300        A smoke test to ensure GET on the add_view works.
 301        """
 302        response = self.client.get(reverse("admin:admin_views_section_add"))
 303        self.assertIsInstance(response, TemplateResponse)
 304        self.assertEqual(response.status_code, 200)
 305
 306    def test_add_with_GET_args(self):
 307        response = self.client.get(
 308            reverse("admin:admin_views_section_add"), {"name": "My Section"}
 309        )
 310        self.assertContains(
 311            response,
 312            'value="My Section"',
 313            msg_prefix="Couldn't find an input with the right value in the response",
 314        )
 315
 316    def test_basic_edit_GET(self):
 317        """
 318        A smoke test to ensure GET on the change_view works.
 319        """
 320        response = self.client.get(
 321            reverse("admin:admin_views_section_change", args=(self.s1.pk,))
 322        )
 323        self.assertIsInstance(response, TemplateResponse)
 324        self.assertEqual(response.status_code, 200)
 325
 326    def test_basic_edit_GET_string_PK(self):
 327        """
 328        GET on the change_view (when passing a string as the PK argument for a
 329        model with an integer PK field) redirects to the index page with a
 330        message saying the object doesn't exist.
 331        """
 332        response = self.client.get(
 333            reverse("admin:admin_views_section_change", args=(quote("abc/<b>"),)),
 334            follow=True,
 335        )
 336        self.assertRedirects(response, reverse("admin:index"))
 337        self.assertEqual(
 338            [m.message for m in response.context["messages"]],
 339            ["section with ID “abc/<b>” doesn’t exist. Perhaps it was deleted?"],
 340        )
 341
 342    def test_basic_edit_GET_old_url_redirect(self):
 343        """
 344        The change URL changed in Django 1.9, but the old one still redirects.
 345        """
 346        response = self.client.get(
 347            reverse("admin:admin_views_section_change", args=(self.s1.pk,)).replace(
 348                "change/", ""
 349            )
 350        )
 351        self.assertRedirects(
 352            response, reverse("admin:admin_views_section_change", args=(self.s1.pk,))
 353        )
 354
 355    def test_basic_inheritance_GET_string_PK(self):
 356        """
 357        GET on the change_view (for inherited models) redirects to the index
 358        page with a message saying the object doesn't exist.
 359        """
 360        response = self.client.get(
 361            reverse("admin:admin_views_supervillain_change", args=("abc",)), follow=True
 362        )
 363        self.assertRedirects(response, reverse("admin:index"))
 364        self.assertEqual(
 365            [m.message for m in response.context["messages"]],
 366            ["super villain with ID “abc” doesn’t exist. Perhaps it was deleted?"],
 367        )
 368
 369    def test_basic_add_POST(self):
 370        """
 371        A smoke test to ensure POST on add_view works.
 372        """
 373        post_data = {
 374            "name": "Another Section",
 375            # inline data
 376            "article_set-TOTAL_FORMS": "3",
 377            "article_set-INITIAL_FORMS": "0",
 378            "article_set-MAX_NUM_FORMS": "0",
 379        }
 380        response = self.client.post(reverse("admin:admin_views_section_add"), post_data)
 381        self.assertEqual(response.status_code, 302)  # redirect somewhere
 382
 383    def test_popup_add_POST(self):
 384        """
 385        Ensure http response from a popup is properly escaped.
 386        """
 387        post_data = {
 388            "_popup": "1",
 389            "title": "title with a new\nline",
 390            "content": "some content",
 391            "date_0": "2010-09-10",
 392            "date_1": "14:55:39",
 393        }
 394        response = self.client.post(reverse("admin:admin_views_article_add"), post_data)
 395        self.assertContains(response, "title with a new\\nline")
 396
 397    def test_basic_edit_POST(self):
 398        """
 399        A smoke test to ensure POST on edit_view works.
 400        """
 401        url = reverse("admin:admin_views_section_change", args=(self.s1.pk,))
 402        response = self.client.post(url, self.inline_post_data)
 403        self.assertEqual(response.status_code, 302)  # redirect somewhere
 404
 405    def test_edit_save_as(self):
 406        """
 407        Test "save as".
 408        """
 409        post_data = self.inline_post_data.copy()
 410        post_data.update(
 411            {
 412                "_saveasnew": "Save+as+new",
 413                "article_set-1-section": "1",
 414                "article_set-2-section": "1",
 415                "article_set-3-section": "1",
 416                "article_set-4-section": "1",
 417                "article_set-5-section": "1",
 418            }
 419        )
 420        response = self.client.post(
 421            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), post_data
 422        )
 423        self.assertEqual(response.status_code, 302)  # redirect somewhere
 424
 425    def test_edit_save_as_delete_inline(self):
 426        """
 427        Should be able to "Save as new" while also deleting an inline.
 428        """
 429        post_data = self.inline_post_data.copy()
 430        post_data.update(
 431            {
 432                "_saveasnew": "Save+as+new",
 433                "article_set-1-section": "1",
 434                "article_set-2-section": "1",
 435                "article_set-2-DELETE": "1",
 436                "article_set-3-section": "1",
 437            }
 438        )
 439        response = self.client.post(
 440            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), post_data
 441        )
 442        self.assertEqual(response.status_code, 302)
 443        # started with 3 articles, one was deleted.
 444        self.assertEqual(Section.objects.latest("id").article_set.count(), 2)
 445
 446    def test_change_list_column_field_classes(self):
 447        response = self.client.get(reverse("admin:admin_views_article_changelist"))
 448        # callables display the callable name.
 449        self.assertContains(response, "column-callable_year")
 450        self.assertContains(response, "field-callable_year")
 451        # lambdas display as "lambda" + index that they appear in list_display.
 452        self.assertContains(response, "column-lambda8")
 453        self.assertContains(response, "field-lambda8")
 454
 455    def test_change_list_sorting_callable(self):
 456        """
 457        Ensure we can sort on a list_display field that is a callable
 458        (column 2 is callable_year in ArticleAdmin)
 459        """
 460        response = self.client.get(
 461            reverse("admin:admin_views_article_changelist"), {"o": 2}
 462        )
 463        self.assertContentBefore(
 464            response,
 465            "Oldest content",
 466            "Middle content",
 467            "Results of sorting on callable are out of order.",
 468        )
 469        self.assertContentBefore(
 470            response,
 471            "Middle content",
 472            "Newest content",
 473            "Results of sorting on callable are out of order.",
 474        )
 475
 476    def test_change_list_sorting_property(self):
 477        """
 478        Sort on a list_display field that is a property (column 10 is
 479        a property in Article model).
 480        """
 481        response = self.client.get(
 482            reverse("admin:admin_views_article_changelist"), {"o": 10}
 483        )
 484        self.assertContentBefore(
 485            response,
 486            "Oldest content",
 487            "Middle content",
 488            "Results of sorting on property are out of order.",
 489        )
 490        self.assertContentBefore(
 491            response,
 492            "Middle content",
 493            "Newest content",
 494            "Results of sorting on property are out of order.",
 495        )
 496
 497    def test_change_list_sorting_callable_query_expression(self):
 498        """Query expressions may be used for admin_order_field."""
 499        tests = [
 500            ("order_by_expression", 9),
 501            ("order_by_f_expression", 12),
 502            ("order_by_orderby_expression", 13),
 503        ]
 504        for admin_order_field, index in tests:
 505            with self.subTest(admin_order_field):
 506                response = self.client.get(
 507                    reverse("admin:admin_views_article_changelist"), {"o": index},
 508                )
 509                self.assertContentBefore(
 510                    response,
 511                    "Oldest content",
 512                    "Middle content",
 513                    "Results of sorting on callable are out of order.",
 514                )
 515                self.assertContentBefore(
 516                    response,
 517                    "Middle content",
 518                    "Newest content",
 519                    "Results of sorting on callable are out of order.",
 520                )
 521
 522    def test_change_list_sorting_callable_query_expression_reverse(self):
 523        tests = [
 524            ("order_by_expression", -9),
 525            ("order_by_f_expression", -12),
 526            ("order_by_orderby_expression", -13),
 527        ]
 528        for admin_order_field, index in tests:
 529            with self.subTest(admin_order_field):
 530                response = self.client.get(
 531                    reverse("admin:admin_views_article_changelist"), {"o": index},
 532                )
 533                self.assertContentBefore(
 534                    response,
 535                    "Middle content",
 536                    "Oldest content",
 537                    "Results of sorting on callable are out of order.",
 538                )
 539                self.assertContentBefore(
 540                    response,
 541                    "Newest content",
 542                    "Middle content",
 543                    "Results of sorting on callable are out of order.",
 544                )
 545
 546    def test_change_list_sorting_model(self):
 547        """
 548        Ensure we can sort on a list_display field that is a Model method
 549        (column 3 is 'model_year' in ArticleAdmin)
 550        """
 551        response = self.client.get(
 552            reverse("admin:admin_views_article_changelist"), {"o": "-3"}
 553        )
 554        self.assertContentBefore(
 555            response,
 556            "Newest content",
 557            "Middle content",
 558            "Results of sorting on Model method are out of order.",
 559        )
 560        self.assertContentBefore(
 561            response,
 562            "Middle content",
 563            "Oldest content",
 564            "Results of sorting on Model method are out of order.",
 565        )
 566
 567    def test_change_list_sorting_model_admin(self):
 568        """
 569        Ensure we can sort on a list_display field that is a ModelAdmin method
 570        (column 4 is 'modeladmin_year' in ArticleAdmin)
 571        """
 572        response = self.client.get(
 573            reverse("admin:admin_views_article_changelist"), {"o": "4"}
 574        )
 575        self.assertContentBefore(
 576            response,
 577            "Oldest content",
 578            "Middle content",
 579            "Results of sorting on ModelAdmin method are out of order.",
 580        )
 581        self.assertContentBefore(
 582            response,
 583            "Middle content",
 584            "Newest content",
 585            "Results of sorting on ModelAdmin method are out of order.",
 586        )
 587
 588    def test_change_list_sorting_model_admin_reverse(self):
 589        """
 590        Ensure we can sort on a list_display field that is a ModelAdmin
 591        method in reverse order (i.e. admin_order_field uses the '-' prefix)
 592        (column 6 is 'model_year_reverse' in ArticleAdmin)
 593        """
 594        response = self.client.get(
 595            reverse("admin:admin_views_article_changelist"), {"o": "6"}
 596        )
 597        self.assertContentBefore(
 598            response,
 599            "2009",
 600            "2008",
 601            "Results of sorting on ModelAdmin method are out of order.",
 602        )
 603        self.assertContentBefore(
 604            response,
 605            "2008",
 606            "2000",
 607            "Results of sorting on ModelAdmin method are out of order.",
 608        )
 609        # Let's make sure the ordering is right and that we don't get a
 610        # FieldError when we change to descending order
 611        response = self.client.get(
 612            reverse("admin:admin_views_article_changelist"), {"o": "-6"}
 613        )
 614        self.assertContentBefore(
 615            response,
 616            "2000",
 617            "2008",
 618            "Results of sorting on ModelAdmin method are out of order.",
 619        )
 620        self.assertContentBefore(
 621            response,
 622            "2008",
 623            "2009",
 624            "Results of sorting on ModelAdmin method are out of order.",
 625        )
 626
 627    def test_change_list_sorting_multiple(self):
 628        p1 = Person.objects.create(name="Chris", gender=1, alive=True)
 629        p2 = Person.objects.create(name="Chris", gender=2, alive=True)
 630        p3 = Person.objects.create(name="Bob", gender=1, alive=True)
 631        link1 = reverse("admin:admin_views_person_change", args=(p1.pk,))
 632        link2 = reverse("admin:admin_views_person_change", args=(p2.pk,))
 633        link3 = reverse("admin:admin_views_person_change", args=(p3.pk,))
 634
 635        # Sort by name, gender
 636        response = self.client.get(
 637            reverse("admin:admin_views_person_changelist"), {"o": "1.2"}
 638        )
 639        self.assertContentBefore(response, link3, link1)
 640        self.assertContentBefore(response, link1, link2)
 641
 642        # Sort by gender descending, name
 643        response = self.client.get(
 644            reverse("admin:admin_views_person_changelist"), {"o": "-2.1"}
 645        )
 646        self.assertContentBefore(response, link2, link3)
 647        self.assertContentBefore(response, link3, link1)
 648
 649    def test_change_list_sorting_preserve_queryset_ordering(self):
 650        """
 651        If no ordering is defined in `ModelAdmin.ordering` or in the query
 652        string, then the underlying order of the queryset should not be
 653        changed, even if it is defined in `Modeladmin.get_queryset()`.
 654        Refs #11868, #7309.
 655        """
 656        p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80)
 657        p2 = Person.objects.create(name="Bob", gender=1, alive=True, age=70)
 658        p3 = Person.objects.create(name="Chris", gender=2, alive=False, age=60)
 659        link1 = reverse("admin:admin_views_person_change", args=(p1.pk,))
 660        link2 = reverse("admin:admin_views_person_change", args=(p2.pk,))
 661        link3 = reverse("admin:admin_views_person_change", args=(p3.pk,))
 662
 663        response = self.client.get(reverse("admin:admin_views_person_changelist"), {})
 664        self.assertContentBefore(response, link3, link2)
 665        self.assertContentBefore(response, link2, link1)
 666
 667    def test_change_list_sorting_model_meta(self):
 668        # Test ordering on Model Meta is respected
 669
 670        l1 = Language.objects.create(iso="ur", name="Urdu")
 671        l2 = Language.objects.create(iso="ar", name="Arabic")
 672        link1 = reverse("admin:admin_views_language_change", args=(quote(l1.pk),))
 673        link2 = reverse("admin:admin_views_language_change", args=(quote(l2.pk),))
 674
 675        response = self.client.get(reverse("admin:admin_views_language_changelist"), {})
 676        self.assertContentBefore(response, link2, link1)
 677
 678        # Test we can override with query string
 679        response = self.client.get(
 680            reverse("admin:admin_views_language_changelist"), {"o": "-1"}
 681        )
 682        self.assertContentBefore(response, link1, link2)
 683
 684    def test_change_list_sorting_override_model_admin(self):
 685        # Test ordering on Model Admin is respected, and overrides Model Meta
 686        dt = datetime.datetime.now()
 687        p1 = Podcast.objects.create(name="A", release_date=dt)
 688        p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
 689        link1 = reverse("admin:admin_views_podcast_change", args=(p1.pk,))
 690        link2 = reverse("admin:admin_views_podcast_change", args=(p2.pk,))
 691
 692        response = self.client.get(reverse("admin:admin_views_podcast_changelist"), {})
 693        self.assertContentBefore(response, link1, link2)
 694
 695    def test_multiple_sort_same_field(self):
 696        # The changelist displays the correct columns if two columns correspond
 697        # to the same ordering field.
 698        dt = datetime.datetime.now()
 699        p1 = Podcast.objects.create(name="A", release_date=dt)
 700        p2 = Podcast.objects.create(name="B", release_date=dt - datetime.timedelta(10))
 701        link1 = reverse("admin:admin_views_podcast_change", args=(quote(p1.pk),))
 702        link2 = reverse("admin:admin_views_podcast_change", args=(quote(p2.pk),))
 703
 704        response = self.client.get(reverse("admin:admin_views_podcast_changelist"), {})
 705        self.assertContentBefore(response, link1, link2)
 706
 707        p1 = ComplexSortedPerson.objects.create(name="Bob", age=10)
 708        p2 = ComplexSortedPerson.objects.create(name="Amy", age=20)
 709        link1 = reverse("admin:admin_views_complexsortedperson_change", args=(p1.pk,))
 710        link2 = reverse("admin:admin_views_complexsortedperson_change", args=(p2.pk,))
 711
 712        response = self.client.get(
 713            reverse("admin:admin_views_complexsortedperson_changelist"), {}
 714        )
 715        # Should have 5 columns (including action checkbox col)
 716        self.assertContains(response, '<th scope="col"', count=5)
 717
 718        self.assertContains(response, "Name")
 719        self.assertContains(response, "Colored name")
 720
 721        # Check order
 722        self.assertContentBefore(response, "Name", "Colored name")
 723
 724        # Check sorting - should be by name
 725        self.assertContentBefore(response, link2, link1)
 726
 727    def test_sort_indicators_admin_order(self):
 728        """
 729        The admin shows default sort indicators for all kinds of 'ordering'
 730        fields: field names, method on the model admin and model itself, and
 731        other callables. See #17252.
 732        """
 733        models = [
 734            (AdminOrderedField, "adminorderedfield"),
 735            (AdminOrderedModelMethod, "adminorderedmodelmethod"),
 736            (AdminOrderedAdminMethod, "adminorderedadminmethod"),
 737            (AdminOrderedCallable, "adminorderedcallable"),
 738        ]
 739        for model, url in models:
 740            model.objects.create(stuff="The Last Item", order=3)
 741            model.objects.create(stuff="The First Item", order=1)
 742            model.objects.create(stuff="The Middle Item", order=2)
 743            response = self.client.get(
 744                reverse("admin:admin_views_%s_changelist" % url), {}
 745            )
 746            self.assertEqual(response.status_code, 200)
 747            # Should have 3 columns including action checkbox col.
 748            self.assertContains(response, '<th scope="col"', count=3, msg_prefix=url)
 749            # Check if the correct column was selected. 2 is the index of the
 750            # 'order' column in the model admin's 'list_display' with 0 being
 751            # the implicit 'action_checkbox' and 1 being the column 'stuff'.
 752            self.assertEqual(
 753                response.context["cl"].get_ordering_field_columns(), {2: "asc"}
 754            )
 755            # Check order of records.
 756            self.assertContentBefore(response, "The First Item", "The Middle Item")
 757            self.assertContentBefore(response, "The Middle Item", "The Last Item")
 758
 759    def test_has_related_field_in_list_display_fk(self):
 760        """Joins shouldn't be performed for <FK>_id fields in list display."""
 761        state = State.objects.create(name="Karnataka")
 762        City.objects.create(state=state, name="Bangalore")
 763        response = self.client.get(reverse("admin:admin_views_city_changelist"), {})
 764
 765        response.context["cl"].list_display = ["id", "name", "state"]
 766        self.assertIs(response.context["cl"].has_related_field_in_list_display(), True)
 767
 768        response.context["cl"].list_display = ["id", "name", "state_id"]
 769        self.assertIs(response.context["cl"].has_related_field_in_list_display(), False)
 770
 771    def test_has_related_field_in_list_display_o2o(self):
 772        """Joins shouldn't be performed for <O2O>_id fields in list display."""
 773        media = Media.objects.create(name="Foo")
 774        Vodcast.objects.create(media=media)
 775        response = self.client.get(reverse("admin:admin_views_vodcast_changelist"), {})
 776
 777        response.context["cl"].list_display = ["media"]
 778        self.assertIs(response.context["cl"].has_related_field_in_list_display(), True)
 779
 780        response.context["cl"].list_display = ["media_id"]
 781        self.assertIs(response.context["cl"].has_related_field_in_list_display(), False)
 782
 783    def test_limited_filter(self):
 784        """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
 785        This also tests relation-spanning filters (e.g. 'color__value').
 786        """
 787        response = self.client.get(reverse("admin:admin_views_thing_changelist"))
 788        self.assertContains(
 789            response,
 790            '<div id="changelist-filter">',
 791            msg_prefix="Expected filter not found in changelist view",
 792        )
 793        self.assertNotContains(
 794            response,
 795            '<a href="?color__id__exact=3">Blue</a>',
 796            msg_prefix="Changelist filter not correctly limited by limit_choices_to",
 797        )
 798
 799    def test_relation_spanning_filters(self):
 800        changelist_url = reverse("admin:admin_views_chapterxtra1_changelist")
 801        response = self.client.get(changelist_url)
 802        self.assertContains(response, '<div id="changelist-filter">')
 803        filters = {
 804            "chap__id__exact": {
 805                "values": [c.id for c in Chapter.objects.all()],
 806                "test": lambda obj, value: obj.chap.id == value,
 807            },
 808            "chap__title": {
 809                "values": [c.title for c in Chapter.objects.all()],
 810                "test": lambda obj, value: obj.chap.title == value,
 811            },
 812            "chap__book__id__exact": {
 813                "values": [b.id for b in Book.objects.all()],
 814                "test": lambda obj, value: obj.chap.book.id == value,
 815            },
 816            "chap__book__name": {
 817                "values": [b.name for b in Book.objects.all()],
 818                "test": lambda obj, value: obj.chap.book.name == value,
 819            },
 820            "chap__book__promo__id__exact": {
 821                "values": [p.id for p in Promo.objects.all()],
 822                "test": lambda obj, value: obj.chap.book.promo_set.filter(
 823                    id=value
 824                ).exists(),
 825            },
 826            "chap__book__promo__name": {
 827                "values": [p.name for p in Promo.objects.all()],
 828                "test": lambda obj, value: obj.chap.book.promo_set.filter(
 829                    name=value
 830                ).exists(),
 831            },
 832            # A forward relation (book) after a reverse relation (promo).
 833            "guest_author__promo__book__id__exact": {
 834                "values": [p.id for p in Book.objects.all()],
 835                "test": lambda obj, value: obj.guest_author.promo_set.filter(
 836                    book=value
 837                ).exists(),
 838            },
 839        }
 840        for filter_path, params in filters.items():
 841            for value in params["values"]:
 842                query_string = urlencode({filter_path: value})
 843                # ensure filter link exists
 844                self.assertContains(response, '<a href="?%s"' % query_string)
 845                # ensure link works
 846                filtered_response = self.client.get(
 847                    "%s?%s" % (changelist_url, query_string)
 848                )
 849                self.assertEqual(filtered_response.status_code, 200)
 850                # ensure changelist contains only valid objects
 851                for obj in filtered_response.context["cl"].queryset.all():
 852                    self.assertTrue(params["test"](obj, value))
 853
 854    def test_incorrect_lookup_parameters(self):
 855        """Ensure incorrect lookup parameters are handled gracefully."""
 856        changelist_url = reverse("admin:admin_views_thing_changelist")
 857        response = self.client.get(changelist_url, {"notarealfield": "5"})
 858        self.assertRedirects(response, "%s?e=1" % changelist_url)
 859
 860        # Spanning relationships through a nonexistent related object (Refs #16716)
 861        response = self.client.get(changelist_url, {"notarealfield__whatever": "5"})
 862        self.assertRedirects(response, "%s?e=1" % changelist_url)
 863
 864        response = self.client.get(
 865            changelist_url, {"color__id__exact": "StringNotInteger!"}
 866        )
 867        self.assertRedirects(response, "%s?e=1" % changelist_url)
 868
 869        # Regression test for #18530
 870        response = self.client.get(changelist_url, {"pub_date__gte": "foo"})
 871        self.assertRedirects(response, "%s?e=1" % changelist_url)
 872
 873    def test_isnull_lookups(self):
 874        """Ensure is_null is handled correctly."""
 875        Article.objects.create(
 876            title="I Could Go Anywhere",
 877            content="Versatile",
 878            date=datetime.datetime.now(),
 879        )
 880        changelist_url = reverse("admin:admin_views_article_changelist")
 881        response = self.client.get(changelist_url)
 882        self.assertContains(response, "4 articles")
 883        response = self.client.get(changelist_url, {"section__isnull": "false"})
 884        self.assertContains(response, "3 articles")
 885        response = self.client.get(changelist_url, {"section__isnull": "0"})
 886        self.assertContains(response, "3 articles")
 887        response = self.client.get(changelist_url, {"section__isnull": "true"})
 888        self.assertContains(response, "1 article")
 889        response = self.client.get(changelist_url, {"section__isnull": "1"})
 890        self.assertContains(response, "1 article")
 891
 892    def test_logout_and_password_change_URLs(self):
 893        response = self.client.get(reverse("admin:admin_views_article_changelist"))
 894        self.assertContains(response, '<a href="%s">' % reverse("admin:logout"))
 895        self.assertContains(
 896            response, '<a href="%s">' % reverse("admin:password_change")
 897        )
 898
 899    def test_named_group_field_choices_change_list(self):
 900        """
 901        Ensures the admin changelist shows correct values in the relevant column
 902        for rows corresponding to instances of a model in which a named group
 903        has been used in the choices option of a field.
 904        """
 905        link1 = reverse("admin:admin_views_fabric_change", args=(self.fab1.pk,))
 906        link2 = reverse("admin:admin_views_fabric_change", args=(self.fab2.pk,))
 907        response = self.client.get(reverse("admin:admin_views_fabric_changelist"))
 908        fail_msg = (
 909            "Changelist table isn't showing the right human-readable values "
 910            "set by a model field 'choices' option named group."
 911        )
 912        self.assertContains(
 913            response,
 914            '<a href="%s">Horizontal</a>' % link1,
 915            msg_prefix=fail_msg,
 916            html=True,
 917        )
 918        self.assertContains(
 919            response,
 920            '<a href="%s">Vertical</a>' % link2,
 921            msg_prefix=fail_msg,
 922            html=True,
 923        )
 924
 925    def test_named_group_field_choices_filter(self):
 926        """
 927        Ensures the filter UI shows correctly when at least one named group has
 928        been used in the choices option of a model field.
 929        """
 930        response = self.client.get(reverse("admin:admin_views_fabric_changelist"))
 931        fail_msg = (
 932            "Changelist filter isn't showing options contained inside a model "
 933            "field 'choices' option named group."
 934        )
 935        self.assertContains(response, '<div id="changelist-filter">')
 936        self.assertContains(
 937            response,
 938            '<a href="?surface__exact=x" title="Horizontal">Horizontal</a>',
 939            msg_prefix=fail_msg,
 940            html=True,
 941        )
 942        self.assertContains(
 943            response,
 944            '<a href="?surface__exact=y" title="Vertical">Vertical</a>',
 945            msg_prefix=fail_msg,
 946            html=True,
 947        )
 948
 949    def test_change_list_null_boolean_display(self):
 950        Post.objects.create(public=None)
 951        response = self.client.get(reverse("admin:admin_views_post_changelist"))
 952        self.assertContains(response, "icon-unknown.svg")
 953
 954    def test_i18n_language_non_english_default(self):
 955        """
 956        Check if the JavaScript i18n view returns an empty language catalog
 957        if the default language is non-English but the selected language
 958        is English. See #13388 and #3594 for more details.
 959        """
 960        with self.settings(LANGUAGE_CODE="fr"), translation.override("en-us"):
 961            response = self.client.get(reverse("admin:jsi18n"))
 962            self.assertNotContains(response, "Choisir une heure")
 963
 964    def test_i18n_language_non_english_fallback(self):
 965        """
 966        Makes sure that the fallback language is still working properly
 967        in cases where the selected language cannot be found.
 968        """
 969        with self.settings(LANGUAGE_CODE="fr"), translation.override("none"):
 970            response = self.client.get(reverse("admin:jsi18n"))
 971            self.assertContains(response, "Choisir une heure")
 972
 973    def test_jsi18n_with_context(self):
 974        response = self.client.get(reverse("admin-extra-context:jsi18n"))
 975        self.assertEqual(response.status_code, 200)
 976
 977    def test_L10N_deactivated(self):
 978        """
 979        Check if L10N is deactivated, the JavaScript i18n view doesn't
 980        return localized date/time formats. Refs #14824.
 981        """
 982        with self.settings(LANGUAGE_CODE="ru", USE_L10N=False), translation.override(
 983            "none"
 984        ):
 985            response = self.client.get(reverse("admin:jsi18n"))
 986            self.assertNotContains(response, "%d.%m.%Y %H:%M:%S")
 987            self.assertContains(response, "%Y-%m-%d %H:%M:%S")
 988
 989    def test_disallowed_filtering(self):
 990        with self.assertLogs("django.security.DisallowedModelAdminLookup", "ERROR"):
 991            response = self.client.get(
 992                "%s?owner__email__startswith=fuzzy"
 993                % reverse("admin:admin_views_album_changelist")
 994            )
 995        self.assertEqual(response.status_code, 400)
 996
 997        # Filters are allowed if explicitly included in list_filter
 998        response = self.client.get(
 999            "%s?color__value__startswith=red"
1000            % reverse("admin:admin_views_thing_changelist")
1001        )
1002        self.assertEqual(response.status_code, 200)
1003        response = self.client.get(
1004            "%s?color__value=red" % reverse("admin:admin_views_thing_changelist")
1005        )
1006        self.assertEqual(response.status_code, 200)
1007
1008        # Filters should be allowed if they involve a local field without the
1009        # need to whitelist them in list_filter or date_hierarchy.
1010        response = self.client.get(
1011            "%s?age__gt=30" % reverse("admin:admin_views_person_changelist")
1012        )
1013        self.assertEqual(response.status_code, 200)
1014
1015        e1 = Employee.objects.create(
1016            name="Anonymous", gender=1, age=22, alive=True, code="123"
1017        )
1018        e2 = Employee.objects.create(
1019            name="Visitor", gender=2, age=19, alive=True, code="124"
1020        )
1021        WorkHour.objects.create(datum=datetime.datetime.now(), employee=e1)
1022        WorkHour.objects.create(datum=datetime.datetime.now(), employee=e2)
1023        response = self.client.get(reverse("admin:admin_views_workhour_changelist"))
1024        self.assertContains(response, "employee__person_ptr__exact")
1025        response = self.client.get(
1026            "%s?employee__person_ptr__exact=%d"
1027            % (reverse("admin:admin_views_workhour_changelist"), e1.pk)
1028        )
1029        self.assertEqual(response.status_code, 200)
1030
1031    def test_disallowed_to_field(self):
1032        url = reverse("admin:admin_views_section_changelist")
1033        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1034            response = self.client.get(url, {TO_FIELD_VAR: "missing_field"})
1035        self.assertEqual(response.status_code, 400)
1036
1037        # Specifying a field that is not referred by any other model registered
1038        # to this admin site should raise an exception.
1039        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1040            response = self.client.get(
1041                reverse("admin:admin_views_section_changelist"), {TO_FIELD_VAR: "name"}
1042            )
1043        self.assertEqual(response.status_code, 400)
1044
1045        # #23839 - Primary key should always be allowed, even if the referenced model isn't registered.
1046        response = self.client.get(
1047            reverse("admin:admin_views_notreferenced_changelist"), {TO_FIELD_VAR: "id"}
1048        )
1049        self.assertEqual(response.status_code, 200)
1050
1051        # #23915 - Specifying a field referenced by another model though a m2m should be allowed.
1052        response = self.client.get(
1053            reverse("admin:admin_views_recipe_changelist"), {TO_FIELD_VAR: "rname"}
1054        )
1055        self.assertEqual(response.status_code, 200)
1056
1057        # #23604, #23915 - Specifying a field referenced through a reverse m2m relationship should be allowed.
1058        response = self.client.get(
1059            reverse("admin:admin_views_ingredient_changelist"), {TO_FIELD_VAR: "iname"}
1060        )
1061        self.assertEqual(response.status_code, 200)
1062
1063        # #23329 - Specifying a field that is not referred by any other model directly registered
1064        # to this admin site but registered through inheritance should be allowed.
1065        response = self.client.get(
1066            reverse("admin:admin_views_referencedbyparent_changelist"),
1067            {TO_FIELD_VAR: "name"},
1068        )
1069        self.assertEqual(response.status_code, 200)
1070
1071        # #23431 - Specifying a field that is only referred to by a inline of a registered
1072        # model should be allowed.
1073        response = self.client.get(
1074            reverse("admin:admin_views_referencedbyinline_changelist"),
1075            {TO_FIELD_VAR: "name"},
1076        )
1077        self.assertEqual(response.status_code, 200)
1078
1079        # #25622 - Specifying a field of a model only referred by a generic
1080        # relation should raise DisallowedModelAdminToField.
1081        url = reverse("admin:admin_views_referencedbygenrel_changelist")
1082        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1083            response = self.client.get(url, {TO_FIELD_VAR: "object_id"})
1084        self.assertEqual(response.status_code, 400)
1085
1086        # We also want to prevent the add, change, and delete views from
1087        # leaking a disallowed field value.
1088        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1089            response = self.client.post(
1090                reverse("admin:admin_views_section_add"), {TO_FIELD_VAR: "name"}
1091            )
1092        self.assertEqual(response.status_code, 400)
1093
1094        section = Section.objects.create()
1095        url = reverse("admin:admin_views_section_change", args=(section.pk,))
1096        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1097            response = self.client.post(url, {TO_FIELD_VAR: "name"})
1098        self.assertEqual(response.status_code, 400)
1099
1100        url = reverse("admin:admin_views_section_delete", args=(section.pk,))
1101        with self.assertLogs("django.security.DisallowedModelAdminToField", "ERROR"):
1102            response = self.client.post(url, {TO_FIELD_VAR: "name"})
1103        self.assertEqual(response.status_code, 400)
1104
1105    def test_allowed_filtering_15103(self):
1106        """
1107        Regressions test for ticket 15103 - filtering on fields defined in a
1108        ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields
1109        can break.
1110        """
1111        # Filters should be allowed if they are defined on a ForeignKey pointing to this model
1112        url = "%s?leader__name=Palin&leader__age=27" % reverse(
1113            "admin:admin_views_inquisition_changelist"
1114        )
1115        response = self.client.get(url)
1116        self.assertEqual(response.status_code, 200)
1117
1118    def test_popup_dismiss_related(self):
1119        """
1120        Regression test for ticket 20664 - ensure the pk is properly quoted.
1121        """
1122        actor = Actor.objects.create(name="Palin", age=27)
1123        response = self.client.get(
1124            "%s?%s" % (reverse("admin:admin_views_actor_changelist"), IS_POPUP_VAR)
1125        )
1126        self.assertContains(response, 'data-popup-opener="%s"' % actor.pk)
1127
1128    def test_hide_change_password(self):
1129        """
1130        Tests if the "change password" link in the admin is hidden if the User
1131        does not have a usable password set.
1132        (against 9bea85795705d015cdadc82c68b99196a8554f5c)
1133        """
1134        user = User.objects.get(username="super")
1135        user.set_unusable_password()
1136        user.save()
1137        self.client.force_login(user)
1138        response = self.client.get(reverse("admin:index"))
1139        self.assertNotContains(
1140            response,
1141            reverse("admin:password_change"),
1142            msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.',
1143        )
1144
1145    def test_change_view_with_show_delete_extra_context(self):
1146        """
1147        The 'show_delete' context variable in the admin's change view controls
1148        the display of the delete button.
1149        """
1150        instance = UndeletableObject.objects.create(name="foo")
1151        response = self.client.get(
1152            reverse("admin:admin_views_undeletableobject_change", args=(instance.pk,))
1153        )
1154        self.assertNotContains(response, "deletelink")
1155
1156    def test_change_view_logs_m2m_field_changes(self):
1157        """Changes to ManyToManyFields are included in the object's history."""
1158        pizza = ReadablePizza.objects.create(name="Cheese")
1159        cheese = Topping.objects.create(name="cheese")
1160        post_data = {"name": pizza.name, "toppings": [cheese.pk]}
1161        response = self.client.post(
1162            reverse("admin:admin_views_readablepizza_change", args=(pizza.pk,)),
1163            post_data,
1164        )
1165        self.assertRedirects(
1166            response, reverse("admin:admin_views_readablepizza_changelist")
1167        )
1168        pizza_ctype = ContentType.objects.get_for_model(
1169            ReadablePizza, for_concrete_model=False
1170        )
1171        log = LogEntry.objects.filter(
1172            content_type=pizza_ctype, object_id=pizza.pk
1173        ).first()
1174        self.assertEqual(log.get_change_message(), "Changed Toppings.")
1175
1176    def test_allows_attributeerror_to_bubble_up(self):
1177        """
1178        AttributeErrors are allowed to bubble when raised inside a change list
1179        view. Requires a model to be created so there's something to display.
1180        Refs: #16655, #18593, and #18747
1181        """
1182        Simple.objects.create()
1183        with self.assertRaises(AttributeError):
1184            self.client.get(reverse("admin:admin_views_simple_changelist"))
1185
1186    def test_changelist_with_no_change_url(self):
1187        """
1188        ModelAdmin.changelist_view shouldn't result in a NoReverseMatch if url
1189        for change_view is removed from get_urls (#20934).
1190        """
1191        o = UnchangeableObject.objects.create()
1192        response = self.client.get(
1193            reverse("admin:admin_views_unchangeableobject_changelist")
1194        )
1195        self.assertEqual(response.status_code, 200)
1196        # Check the format of the shown object -- shouldn't contain a change link
1197        self.assertContains(
1198            response, '<th class="field-__str__">%s</th>' % o, html=True
1199        )
1200
1201    def test_invalid_appindex_url(self):
1202        """
1203        #21056 -- URL reversing shouldn't work for nonexistent apps.
1204        """
1205        good_url = "/test_admin/admin/admin_views/"
1206        confirm_good_url = reverse(
1207            "admin:app_list", kwargs={"app_label": "admin_views"}
1208        )
1209        self.assertEqual(good_url, confirm_good_url)
1210
1211        with self.assertRaises(NoReverseMatch):
1212            reverse("admin:app_list", kwargs={"app_label": "this_should_fail"})
1213        with self.assertRaises(NoReverseMatch):
1214            reverse("admin:app_list", args=("admin_views2",))
1215
1216    def test_resolve_admin_views(self):
1217        index_match = resolve("/test_admin/admin4/")
1218        list_match = resolve("/test_admin/admin4/auth/user/")
1219        self.assertIs(index_match.func.admin_site, customadmin.simple_site)
1220        self.assertIsInstance(
1221            list_match.func.model_admin, customadmin.CustomPwdTemplateUserAdmin
1222        )
1223
1224    def test_adminsite_display_site_url(self):
1225        """
1226        #13749 - Admin should display link to front-end site 'View site'
1227        """
1228        url = reverse("admin:index")
1229        response = self.client.get(url)
1230        self.assertEqual(response.context["site_url"], "/my-site-url/")
1231        self.assertContains(response, '<a href="/my-site-url/">View site</a>')
1232
1233    @override_settings(TIME_ZONE="America/Sao_Paulo", USE_TZ=True)
1234    def test_date_hierarchy_timezone_dst(self):
1235        # This datetime doesn't exist in this timezone due to DST.
1236        date = pytz.timezone("America/Sao_Paulo").localize(
1237            datetime.datetime(2016, 10, 16, 15), is_dst=None
1238        )
1239        q = Question.objects.create(question="Why?", expires=date)
1240        Answer2.objects.create(question=q, answer="Because.")
1241        response = self.client.get(reverse("admin:admin_views_answer2_changelist"))
1242        self.assertEqual(response.status_code, 200)
1243        self.assertContains(response, "question__expires__day=16")
1244        self.assertContains(response, "question__expires__month=10")
1245        self.assertContains(response, "question__expires__year=2016")
1246
1247    def test_sortable_by_columns_subset(self):
1248        expected_sortable_fields = ("date", "callable_year")
1249        expected_not_sortable_fields = (
1250            "content",
1251            "model_year",
1252            "modeladmin_year",
1253            "model_year_reversed",
1254            "section",
1255        )
1256        response = self.client.get(reverse("admin6:admin_views_article_changelist"))
1257        for field_name in expected_sortable_fields:
1258            self.assertContains(
1259                response, '<th scope="col"  class="sortable column-%s">' % field_name
1260            )
1261        for field_name in expected_not_sortable_fields:
1262            self.assertContains(
1263                response, '<th scope="col"  class="column-%s">' % field_name
1264            )
1265
1266    def test_get_sortable_by_columns_subset(self):
1267        response = self.client.get(reverse("admin6:admin_views_actor_changelist"))
1268        self.assertContains(response, '<th scope="col"  class="sortable column-age">')
1269        self.assertContains(response, '<th scope="col"  class="column-name">')
1270
1271    def test_sortable_by_no_column(self):
1272        expected_not_sortable_fields = ("title", "book")
1273        response = self.client.get(reverse("admin6:admin_views_chapter_changelist"))
1274        for field_name in expected_not_sortable_fields:
1275            self.assertContains(
1276                response, '<th scope="col"  class="column-%s">' % field_name
1277            )
1278        self.assertNotContains(response, '<th scope="col"  class="sortable column')
1279
1280    def test_get_sortable_by_no_column(self):
1281        response = self.client.get(reverse("admin6:admin_views_color_changelist"))
1282        self.assertContains(response, '<th scope="col"  class="column-value">')
1283        self.assertNotContains(response, '<th scope="col"  class="sortable column')
1284
1285
1286@override_settings(
1287    TEMPLATES=[
1288        {
1289            "BACKEND": "django.template.backends.django.DjangoTemplates",
1290            # Put this app's and the shared tests templates dirs in DIRS to take precedence
1291            # over the admin's templates dir.
1292            "DIRS": [
1293                os.path.join(os.path.dirname(__file__), "templates"),
1294                os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates"),
1295            ],
1296            "APP_DIRS": True,
1297            "OPTIONS": {
1298                "context_processors": [
1299                    "django.template.context_processors.debug",
1300                    "django.template.context_processors.request",
1301                    "django.contrib.auth.context_processors.auth",
1302                    "django.contrib.messages.context_processors.messages",
1303                ],
1304            },
1305        }
1306    ]
1307)
1308class AdminCustomTemplateTests(AdminViewBasicTestCase):
1309    def test_custom_model_admin_templates(self):
1310        # Test custom change list template with custom extra context
1311        response = self.client.get(
1312            reverse("admin:admin_views_customarticle_changelist")
1313        )
1314        self.assertContains(response, "var hello = 'Hello!';")
1315        self.assertTemplateUsed(response, "custom_admin/change_list.html")
1316
1317        # Test custom add form template
1318        response = self.client.get(reverse("admin:admin_views_customarticle_add"))
1319        self.assertTemplateUsed(response, "custom_admin/add_form.html")
1320
1321        # Add an article so we can test delete, change, and history views
1322        post = self.client.post(
1323            reverse("admin:admin_views_customarticle_add"),
1324            {
1325                "content": "<p>great article</p>",
1326                "date_0": "2008-03-18",
1327                "date_1": "10:54:39",
1328            },
1329        )
1330        self.assertRedirects(
1331            post, reverse("admin:admin_views_customarticle_changelist")
1332        )
1333        self.assertEqual(CustomArticle.objects.all().count(), 1)
1334        article_pk = CustomArticle.objects.all()[0].pk
1335
1336        # Test custom delete, change, and object history templates
1337        # Test custom change form template
1338        response = self.client.get(
1339            reverse("admin:admin_views_customarticle_change", args=(article_pk,))
1340        )
1341        self.assertTemplateUsed(response, "custom_admin/change_form.html")
1342        response = self.client.get(
1343            reverse("admin:admin_views_customarticle_delete", args=(article_pk,))
1344        )
1345        self.assertTemplateUsed(response, "custom_admin/delete_confirmation.html")
1346        response = self.client.post(
1347            reverse("admin:admin_views_customarticle_changelist"),
1348            data={
1349                "index": 0,
1350                "action": ["delete_selected"],
1351                "_selected_action": ["1"],
1352            },
1353        )
1354        self.assertTemplateUsed(
1355            response, "custom_admin/delete_selected_confirmation.html"
1356        )
1357        response = self.client.get(
1358            reverse("admin:admin_views_customarticle_history", args=(article_pk,))
1359        )
1360        self.assertTemplateUsed(response, "custom_admin/object_history.html")
1361
1362        # A custom popup response template may be specified by
1363        # ModelAdmin.popup_response_template.
1364        response = self.client.post(
1365            reverse("admin:admin_views_customarticle_add") + "?%s=1" % IS_POPUP_VAR,
1366            {
1367                "content": "<p>great article</p>",
1368                "date_0": "2008-03-18",
1369                "date_1": "10:54:39",
1370                IS_POPUP_VAR: "1",
1371            },
1372        )
1373        self.assertEqual(response.template_name, "custom_admin/popup_response.html")
1374
1375    def test_extended_bodyclass_template_change_form(self):
1376        """
1377        The admin/change_form.html template uses block.super in the
1378        bodyclass block.
1379        """
1380        response = self.client.get(reverse("admin:admin_views_section_add"))
1381        self.assertContains(response, "bodyclass_consistency_check ")
1382
1383    def test_change_password_template(self):
1384        user = User.objects.get(username="super")
1385        response = self.client.get(
1386            reverse("admin:auth_user_password_change", args=(user.id,))
1387        )
1388        # The auth/user/change_password.html template uses super in the
1389        # bodyclass block.
1390        self.assertContains(response, "bodyclass_consistency_check ")
1391
1392        # When a site has multiple passwords in the browser's password manager,
1393        # a browser pop up asks which user the new password is for. To prevent
1394        # this, the username is added to the change password form.
1395        self.assertContains(
1396            response,
1397            '<input type="text" name="username" value="super" style="display: none">',
1398        )
1399
1400    def test_extended_bodyclass_template_index(self):
1401        """
1402        The admin/index.html template uses block.super in the bodyclass block.
1403        """
1404        response = self.client.get(reverse("admin:index"))
1405        self.assertContains(response, "bodyclass_consistency_check ")
1406
1407    def test_extended_bodyclass_change_list(self):
1408        """
1409        The admin/change_list.html' template uses block.super
1410        in the bodyclass block.
1411        """
1412        response = self.client.get(reverse("admin:admin_views_article_changelist"))
1413        self.assertContains(response, "bodyclass_consistency_check ")
1414
1415    def test_extended_bodyclass_template_login(self):
1416        """
1417        The admin/login.html template uses block.super in the
1418        bodyclass block.
1419        """
1420        self.client.logout()
1421        response = self.client.get(reverse("admin:login"))
1422        self.assertContains(response, "bodyclass_consistency_check ")
1423
1424    def test_extended_bodyclass_template_delete_confirmation(self):
1425        """
1426        The admin/delete_confirmation.html template uses
1427        block.super in the bodyclass block.
1428        """
1429        group = Group.objects.create(name="foogroup")
1430        response = self.client.get(reverse("admin:auth_group_delete", args=(group.id,)))
1431        self.assertContains(response, "bodyclass_consistency_check ")
1432
1433    def test_extended_bodyclass_template_delete_selected_confirmation(self):
1434        """
1435        The admin/delete_selected_confirmation.html template uses
1436        block.super in bodyclass block.
1437        """
1438        group = Group.objects.create(name="foogroup")
1439        post_data = {
1440            "action": "delete_selected",
1441            "selected_across": "0",
1442            "index": "0",
1443            "_selected_action": group.id,
1444        }
1445        response = self.client.post(reverse("admin:auth_group_changelist"), post_data)
1446        self.assertEqual(response.context["site_header"], "Django administration")
1447        self.assertContains(response, "bodyclass_consistency_check ")
1448
1449    def test_filter_with_custom_template(self):
1450        """
1451        A custom template can be used to render an admin filter.
1452        """
1453        response = self.client.get(reverse("admin:admin_views_color2_changelist"))
1454        self.assertTemplateUsed(response, "custom_filter_template.html")
1455
1456
1457@override_settings(ROOT_URLCONF="admin_views.urls")
1458class AdminViewFormUrlTest(TestCase):
1459    current_app = "admin3"
1460
1461    @classmethod
1462    def setUpTestData(cls):
1463        cls.superuser = User.objects.create_superuser(
1464            username="super", password="secret", email="super@example.com"
1465        )
1466        cls.s1 = Section.objects.create(name="Test section")
1467        cls.a1 = Article.objects.create(
1468            content="<p>Middle content</p>",
1469            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
1470            section=cls.s1,
1471        )
1472        cls.a2 = Article.objects.create(
1473            content="<p>Oldest content</p>",
1474            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
1475            section=cls.s1,
1476        )
1477        cls.a3 = Article.objects.create(
1478            content="<p>Newest content</p>",
1479            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
1480            section=cls.s1,
1481        )
1482        cls.p1 = PrePopulatedPost.objects.create(
1483            title="A Long Title", published=True, slug="a-long-title"
1484        )
1485
1486    def setUp(self):
1487        self.client.force_login(self.superuser)
1488
1489    def test_change_form_URL_has_correct_value(self):
1490        """
1491        change_view has form_url in response.context
1492        """
1493        response = self.client.get(
1494            reverse(
1495                "admin:admin_views_section_change",
1496                args=(self.s1.pk,),
1497                current_app=self.current_app,
1498            )
1499        )
1500        self.assertIn(
1501            "form_url", response.context, msg="form_url not present in response.context"
1502        )
1503        self.assertEqual(response.context["form_url"], "pony")
1504
1505    def test_initial_data_can_be_overridden(self):
1506        """
1507        The behavior for setting initial form data can be overridden in the
1508        ModelAdmin class. Usually, the initial value is set via the GET params.
1509        """
1510        response = self.client.get(
1511            reverse("admin:admin_views_restaurant_add", current_app=self.current_app),
1512            {"name": "test_value"},
1513        )
1514        # this would be the usual behaviour
1515        self.assertNotContains(response, 'value="test_value"')
1516        # this is the overridden behaviour
1517        self.assertContains(response, 'value="overridden_value"')
1518
1519
1520@override_settings(ROOT_URLCONF="admin_views.urls")
1521class AdminJavaScriptTest(TestCase):
1522    @classmethod
1523    def setUpTestData(cls):
1524        cls.superuser = User.objects.create_superuser(
1525            username="super", password="secret", email="super@example.com"
1526        )
1527
1528    def setUp(self):
1529        self.client.force_login(self.superuser)
1530
1531    def test_js_minified_only_if_debug_is_false(self):
1532        """
1533        The minified versions of the JS files are only used when DEBUG is False.
1534        """
1535        with override_settings(DEBUG=False):
1536            response = self.client.get(reverse("admin:admin_views_section_add"))
1537            self.assertNotContains(response, "vendor/jquery/jquery.js")
1538            self.assertContains(response, "vendor/jquery/jquery.min.js")
1539            self.assertNotContains(response, "prepopulate.js")
1540            self.assertContains(response, "prepopulate.min.js")
1541            self.assertNotContains(response, "actions.js")
1542            self.assertContains(response, "actions.min.js")
1543            self.assertNotContains(response, "collapse.js")
1544            self.assertContains(response, "collapse.min.js")
1545            self.assertNotContains(response, "inlines.js")
1546            self.assertContains(response, "inlines.min.js")
1547        with override_settings(DEBUG=True):
1548            response = self.client.get(reverse("admin:admin_views_section_add"))
1549            self.assertContains(response, "vendor/jquery/jquery.js")
1550            self.assertNotContains(response, "vendor/jquery/jquery.min.js")
1551            self.assertContains(response, "prepopulate.js")
1552            self.assertNotContains(response, "prepopulate.min.js")
1553            self.assertContains(response, "actions.js")
1554            self.assertNotContains(response, "actions.min.js")
1555            self.assertContains(response, "collapse.js")
1556            self.assertNotContains(response, "collapse.min.js")
1557            self.assertContains(response, "inlines.js")
1558            self.assertNotContains(response, "inlines.min.js")
1559
1560
1561@override_settings(ROOT_URLCONF="admin_views.urls")
1562class SaveAsTests(TestCase):
1563    @classmethod
1564    def setUpTestData(cls):
1565        cls.superuser = User.objects.create_superuser(
1566            username="super", password="secret", email="super@example.com"
1567        )
1568        cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
1569
1570    def setUp(self):
1571        self.client.force_login(self.superuser)
1572
1573    def test_save_as_duplication(self):
1574        """'save as' creates a new person"""
1575        post_data = {"_saveasnew": "", "name": "John M", "gender": 1, "age": 42}
1576        response = self.client.post(
1577            reverse("admin:admin_views_person_change", args=(self.per1.pk,)), post_data
1578        )
1579        self.assertEqual(len(Person.objects.filter(name="John M")), 1)
1580        self.assertEqual(len(Person.objects.filter(id=self.per1.pk)), 1)
1581        new_person = Person.objects.latest("id")
1582        self.assertRedirects(
1583            response, reverse("admin:admin_views_person_change", args=(new_person.pk,))
1584        )
1585
1586    def test_save_as_continue_false(self):
1587        """
1588        Saving a new object using "Save as new" redirects to the changelist
1589        instead of the change view when ModelAdmin.save_as_continue=False.
1590        """
1591        post_data = {"_saveasnew": "", "name": "John M", "gender": 1, "age": 42}
1592        url = reverse(
1593            "admin:admin_views_person_change",
1594            args=(self.per1.pk,),
1595            current_app=site2.name,
1596        )
1597        response = self.client.post(url, post_data)
1598        self.assertEqual(len(Person.objects.filter(name="John M")), 1)
1599        self.assertEqual(len(Person.objects.filter(id=self.per1.pk)), 1)
1600        self.assertRedirects(
1601            response,
1602            reverse("admin:admin_views_person_changelist", current_app=site2.name),
1603        )
1604
1605    def test_save_as_new_with_validation_errors(self):
1606        """
1607        When you click "Save as new" and have a validation error,
1608        you only see the "Save as new" button and not the other save buttons,
1609        and that only the "Save as" button is visible.
1610        """
1611        response = self.client.post(
1612            reverse("admin:admin_views_person_change", args=(self.per1.pk,)),
1613            {"_saveasnew": "", "gender": "invalid", "_addanother": "fail",},
1614        )
1615        self.assertContains(response, "Please correct the errors below.")
1616        self.assertFalse(response.context["show_save_and_add_another"])
1617        self.assertFalse(response.context["show_save_and_continue"])
1618        self.assertTrue(response.context["show_save_as_new"])
1619
1620    def test_save_as_new_with_validation_errors_with_inlines(self):
1621        parent = Parent.objects.create(name="Father")
1622        child = Child.objects.create(parent=parent, name="Child")
1623        response = self.client.post(
1624            reverse("admin:admin_views_parent_change", args=(parent.pk,)),
1625            {
1626                "_saveasnew": "Save as new",
1627                "child_set-0-parent": parent.pk,
1628                "child_set-0-id": child.pk,
1629                "child_set-0-name": "Child",
1630                "child_set-INITIAL_FORMS": 1,
1631                "child_set-MAX_NUM_FORMS": 1000,
1632                "child_set-MIN_NUM_FORMS": 0,
1633                "child_set-TOTAL_FORMS": 4,
1634                "name": "_invalid",
1635            },
1636        )
1637        self.assertContains(response, "Please correct the error below.")
1638        self.assertFalse(response.context["show_save_and_add_another"])
1639        self.assertFalse(response.context["show_save_and_continue"])
1640        self.assertTrue(response.context["show_save_as_new"])
1641
1642    def test_save_as_new_with_inlines_with_validation_errors(self):
1643        parent = Parent.objects.create(name="Father")
1644        child = Child.objects.create(parent=parent, name="Child")
1645        response = self.client.post(
1646            reverse("admin:admin_views_parent_change", args=(parent.pk,)),
1647            {
1648                "_saveasnew": "Save as new",
1649                "child_set-0-parent": parent.pk,
1650                "child_set-0-id": child.pk,
1651                "child_set-0-name": "_invalid",
1652                "child_set-INITIAL_FORMS": 1,
1653                "child_set-MAX_NUM_FORMS": 1000,
1654                "child_set-MIN_NUM_FORMS": 0,
1655                "child_set-TOTAL_FORMS": 4,
1656                "name": "Father",
1657            },
1658        )
1659        self.assertContains(response, "Please correct the error below.")
1660        self.assertFalse(response.context["show_save_and_add_another"])
1661        self.assertFalse(response.context["show_save_and_continue"])
1662        self.assertTrue(response.context["show_save_as_new"])
1663
1664
1665@override_settings(ROOT_URLCONF="admin_views.urls")
1666class CustomModelAdminTest(AdminViewBasicTestCase):
1667    def test_custom_admin_site_login_form(self):
1668        self.client.logout()
1669        response = self.client.get(reverse("admin2:index"), follow=True)
1670        self.assertIsInstance(response, TemplateResponse)
1671        self.assertEqual(response.status_code, 200)
1672        login = self.client.post(
1673            reverse("admin2:login"),
1674            {
1675                REDIRECT_FIELD_NAME: reverse("admin2:index"),
1676                "username": "customform",
1677                "password": "secret",
1678            },
1679            follow=True,
1680        )
1681        self.assertIsInstance(login, TemplateResponse)
1682        self.assertEqual(login.status_code, 200)
1683        self.assertContains(login, "custom form error")
1684        self.assertContains(login, "path/to/media.css")
1685
1686    def test_custom_admin_site_login_template(self):
1687        self.client.logout()
1688        response = self.client.get(reverse("admin2:index"), follow=True)
1689        self.assertIsInstance(response, TemplateResponse)
1690        self.assertTemplateUsed(response, "custom_admin/login.html")
1691        self.assertContains(response, "Hello from a custom login template")
1692
1693    def test_custom_admin_site_logout_template(self):
1694        response = self.client.get(reverse("admin2:logout"))
1695        self.assertIsInstance(response, TemplateResponse)
1696        self.assertTemplateUsed(response, "custom_admin/logout.html")
1697        self.assertContains(response, "Hello from a custom logout template")
1698
1699    def test_custom_admin_site_index_view_and_template(self):
1700        response = self.client.get(reverse("admin2:index"))
1701        self.assertIsInstance(response, TemplateResponse)
1702        self.assertTemplateUsed(response, "custom_admin/index.html")
1703        self.assertContains(response, "Hello from a custom index template *bar*")
1704
1705    def test_custom_admin_site_app_index_view_and_template(self):
1706        response = self.client.get(reverse("admin2:app_list", args=("admin_views",)))
1707        self.assertIsInstance(response, TemplateResponse)
1708        self.assertTemplateUsed(response, "custom_admin/app_index.html")
1709        self.assertContains(response, "Hello from a custom app_index template")
1710
1711    def test_custom_admin_site_password_change_template(self):
1712        response = self.client.get(reverse("admin2:password_change"))
1713        self.assertIsInstance(response, TemplateResponse)
1714        self.assertTemplateUsed(response, "custom_admin/password_change_form.html")
1715        self.assertContains(
1716            response, "Hello from a custom password change form template"
1717        )
1718
1719    def test_custom_admin_site_password_change_with_extra_context(self):
1720        response = self.client.get(reverse("admin2:password_change"))
1721        self.assertIsInstance(response, TemplateResponse)
1722        self.assertTemplateUsed(response, "custom_admin/password_change_form.html")
1723        self.assertContains(response, "eggs")
1724
1725    def test_custom_admin_site_password_change_done_template(self):
1726        response = self.client.get(reverse("admin2:password_change_done"))
1727        self.assertIsInstance(response, TemplateResponse)
1728        self.assertTemplateUsed(response, "custom_admin/password_change_done.html")
1729        self.assertContains(
1730            response, "Hello from a custom password change done template"
1731        )
1732
1733    def test_custom_admin_site_view(self):
1734        self.client.force_login(self.superuser)
1735        response = self.client.get(reverse("admin2:my_view"))
1736        self.assertEqual(response.content, b"Django is a magical pony!")
1737
1738    def test_pwd_change_custom_template(self):
1739        self.client.force_login(self.superuser)
1740        su = User.objects.get(username="super")
1741        response = self.client.get(
1742            reverse("admin4:auth_user_password_change", args=(su.pk,))
1743        )
1744        self.assertEqual(response.status_code, 200)
1745
1746
1747def get_perm(Model, codename):
1748    """Return the permission object, for the Model"""
1749    ct = ContentType.objects.get_for_model(Model, for_concrete_model=False)
1750    return Permission.objects.get(content_type=ct, codename=codename)
1751
1752
1753@override_settings(
1754    ROOT_URLCONF="admin_views.urls",
1755    # Test with the admin's documented list of required context processors.
1756    TEMPLATES=[
1757        {
1758            "BACKEND": "django.template.backends.django.DjangoTemplates",
1759            "APP_DIRS": True,
1760            "OPTIONS": {
1761                "context_processors": [
1762                    "django.contrib.auth.context_processors.auth",
1763                    "django.contrib.messages.context_processors.messages",
1764                ],
1765            },
1766        }
1767    ],
1768)
1769class AdminViewPermissionsTest(TestCase):
1770    """Tests for Admin Views Permissions."""
1771
1772    @classmethod
1773    def setUpTestData(cls):
1774        cls.superuser = User.objects.create_superuser(
1775            username="super", password="secret", email="super@example.com"
1776        )
1777        cls.viewuser = User.objects.create_user(
1778            username="viewuser", password="secret", is_staff=True
1779        )
1780        cls.adduser = User.objects.create_user(
1781            username="adduser", password="secret", is_staff=True
1782        )
1783        cls.changeuser = User.objects.create_user(
1784            username="changeuser", password="secret", is_staff=True
1785        )
1786        cls.deleteuser = User.objects.create_user(
1787            username="deleteuser", password="secret", is_staff=True
1788        )
1789        cls.joepublicuser = User.objects.create_user(
1790            username="joepublic", password="secret"
1791        )
1792        cls.nostaffuser = User.objects.create_user(
1793            username="nostaff", password="secret"
1794        )
1795        cls.s1 = Section.objects.create(name="Test section")
1796        cls.a1 = Article.objects.create(
1797            content="<p>Middle content</p>",
1798            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
1799            section=cls.s1,
1800            another_section=cls.s1,
1801        )
1802        cls.a2 = Article.objects.create(
1803            content="<p>Oldest content</p>",
1804            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
1805            section=cls.s1,
1806        )
1807        cls.a3 = Article.objects.create(
1808            content="<p>Newest content</p>",
1809            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
1810            section=cls.s1,
1811        )
1812        cls.p1 = PrePopulatedPost.objects.create(
1813            title="A Long Title", published=True, slug="a-long-title"
1814        )
1815
1816        # Setup permissions, for our users who can add, change, and delete.
1817        opts = Article._meta
1818
1819        # User who can view Articles
1820        cls.viewuser.user_permissions.add(
1821            get_perm(Article, get_permission_codename("view", opts))
1822        )
1823        # User who can add Articles
1824        cls.adduser.user_permissions.add(
1825            get_perm(Article, get_permission_codename("add", opts))
1826        )
1827        # User who can change Articles
1828        cls.changeuser.user_permissions.add(
1829            get_perm(Article, get_permission_codename("change", opts))
1830        )
1831        cls.nostaffuser.user_permissions.add(
1832            get_perm(Article, get_permission_codename("change", opts))
1833        )
1834
1835        # User who can delete Articles
1836        cls.deleteuser.user_permissions.add(
1837            get_perm(Article, get_permission_codename("delete", opts))
1838        )
1839        cls.deleteuser.user_permissions.add(
1840            get_perm(Section, get_permission_codename("delete", Section._meta))
1841        )
1842
1843        # login POST dicts
1844        cls.index_url = reverse("admin:index")
1845        cls.super_login = {
1846            REDIRECT_FIELD_NAME: cls.index_url,
1847            "username": "super",
1848            "password": "secret",
1849        }
1850        cls.super_email_login = {
1851            REDIRECT_FIELD_NAME: cls.index_url,
1852            "username": "super@example.com",
1853            "password": "secret",
1854        }
1855        cls.super_email_bad_login = {
1856            REDIRECT_FIELD_NAME: cls.index_url,
1857            "username": "super@example.com",
1858            "password": "notsecret",
1859        }
1860        cls.adduser_login = {
1861            REDIRECT_FIELD_NAME: cls.index_url,
1862            "username": "adduser",
1863            "password": "secret",
1864        }
1865        cls.changeuser_login = {
1866            REDIRECT_FIELD_NAME: cls.index_url,
1867            "username": "changeuser",
1868            "password": "secret",
1869        }
1870        cls.deleteuser_login = {
1871            REDIRECT_FIELD_NAME: cls.index_url,
1872            "username": "deleteuser",
1873            "password": "secret",
1874        }
1875        cls.nostaff_login = {
1876            REDIRECT_FIELD_NAME: reverse("has_permission_admin:index"),
1877            "username": "nostaff",
1878            "password": "secret",
1879        }
1880        cls.joepublic_login = {
1881            REDIRECT_FIELD_NAME: cls.index_url,
1882            "username": "joepublic",
1883            "password": "secret",
1884        }
1885        cls.viewuser_login = {
1886            REDIRECT_FIELD_NAME: cls.index_url,
1887            "username": "viewuser",
1888            "password": "secret",
1889        }
1890        cls.no_username_login = {
1891            REDIRECT_FIELD_NAME: cls.index_url,
1892            "password": "secret",
1893        }
1894
1895    def test_login(self):
1896        """
1897        Make sure only staff members can log in.
1898
1899        Successful posts to the login page will redirect to the original url.
1900        Unsuccessful attempts will continue to render the login page with
1901        a 200 status code.
1902        """
1903        login_url = "%s?next=%s" % (reverse("admin:login"), reverse("admin:index"))
1904        # Super User
1905        response = self.client.get(self.index_url)
1906        self.assertRedirects(response, login_url)
1907        login = self.client.post(login_url, self.super_login)
1908        self.assertRedirects(login, self.index_url)
1909        self.assertFalse(login.context)
1910        self.client.get(reverse("admin:logout"))
1911
1912        # Test if user enters email address
1913        response = self.client.get(self.index_url)
1914        self.assertEqual(response.status_code, 302)
1915        login = self.client.post(login_url, self.super_email_login)
1916        self.assertContains(login, ERROR_MESSAGE)
1917        # only correct passwords get a username hint
1918        login = self.client.post(login_url, self.super_email_bad_login)
1919        self.assertContains(login, ERROR_MESSAGE)
1920        new_user = User(username="jondoe", password="secret", email="super@example.com")
1921        new_user.save()
1922        # check to ensure if there are multiple email addresses a user doesn't get a 500
1923        login = self.client.post(login_url, self.super_email_login)
1924        self.assertContains(login, ERROR_MESSAGE)
1925
1926        # View User
1927        response = self.client.get(self.index_url)
1928        self.assertEqual(response.status_code, 302)
1929        login = self.client.post(login_url, self.viewuser_login)
1930        self.assertRedirects(login, self.index_url)
1931        self.assertFalse(login.context)
1932        self.client.get(reverse("admin:logout"))
1933
1934        # Add User
1935        response = self.client.get(self.index_url)
1936        self.assertEqual(response.status_code, 302)
1937        login = self.client.post(login_url, self.adduser_login)
1938        self.assertRedirects(login, self.index_url)
1939        self.assertFalse(login.context)
1940        self.client.get(reverse("admin:logout"))
1941
1942        # Change User
1943        response = self.client.get(self.index_url)
1944        self.assertEqual(response.status_code, 302)
1945        login = self.client.post(login_url, self.changeuser_login)
1946        self.assertRedirects(login, self.index_url)
1947        self.assertFalse(login.context)
1948        self.client.get(reverse("admin:logout"))
1949
1950        # Delete User
1951        response = self.client.get(self.index_url)
1952        self.assertEqual(response.status_code, 302)
1953        login = self.client.post(login_url, self.deleteuser_login)
1954        self.assertRedirects(login, self.index_url)
1955        self.assertFalse(login.context)
1956        self.client.get(reverse("admin:logout"))
1957
1958        # Regular User should not be able to login.
1959        response = self.client.get(self.index_url)
1960        self.assertEqual(response.status_code, 302)
1961        login = self.client.post(login_url, self.joepublic_login)
1962        self.assertEqual(login.status_code, 200)
1963        self.assertContains(login, ERROR_MESSAGE)
1964
1965        # Requests without username should not return 500 errors.
1966        response = self.client.get(self.index_url)
1967        self.assertEqual(response.status_code, 302)
1968        login = self.client.post(login_url, self.no_username_login)
1969        self.assertEqual(login.status_code, 200)
1970        self.assertFormError(login, "form", "username", ["This field is required."])
1971
1972    def test_login_redirect_for_direct_get(self):
1973        """
1974        Login redirect should be to the admin index page when going directly to
1975        /admin/login/.
1976        """
1977        response = self.client.get(reverse("admin:login"))
1978        self.assertEqual(response.status_code, 200)
1979        self.assertEqual(response.context[REDIRECT_FIELD_NAME], reverse("admin:index"))
1980
1981    def test_login_has_permission(self):
1982        # Regular User should not be able to login.
1983        response = self.client.get(reverse("has_permission_admin:index"))
1984        self.assertEqual(response.status_code, 302)
1985        login = self.client.post(
1986            reverse("has_permission_admin:login"), self.joepublic_login
1987        )
1988        self.assertEqual(login.status_code, 200)
1989        self.assertContains(login, "permission denied")
1990
1991        # User with permissions should be able to login.
1992        response = self.client.get(reverse("has_permission_admin:index"))
1993        self.assertEqual(response.status_code, 302)
1994        login = self.client.post(
1995            reverse("has_permission_admin:login"), self.nostaff_login
1996        )
1997        self.assertRedirects(login, reverse("has_permission_admin:index"))
1998        self.assertFalse(login.context)
1999        self.client.get(reverse("has_permission_admin:logout"))
2000
2001        # Staff should be able to login.
2002        response = self.client.get(reverse("has_permission_admin:index"))
2003        self.assertEqual(response.status_code, 302)
2004        login = self.client.post(
2005            reverse("has_permission_admin:login"),
2006            {
2007                REDIRECT_FIELD_NAME: reverse("has_permission_admin:index"),
2008                "username": "deleteuser",
2009                "password": "secret",
2010            },
2011        )
2012        self.assertRedirects(login, reverse("has_permission_admin:index"))
2013        self.assertFalse(login.context)
2014        self.client.get(reverse("has_permission_admin:logout"))
2015
2016    def test_login_successfully_redirects_to_original_URL(self):
2017        response = self.client.get(self.index_url)
2018        self.assertEqual(response.status_code, 302)
2019        query_string = "the-answer=42"
2020        redirect_url = "%s?%s" % (self.index_url, query_string)
2021        new_next = {REDIRECT_FIELD_NAME: redirect_url}
2022        post_data = self.super_login.copy()
2023        post_data.pop(REDIRECT_FIELD_NAME)
2024        login = self.client.post(
2025            "%s?%s" % (reverse("admin:login"), urlencode(new_next)), post_data
2026        )
2027        self.assertRedirects(login, redirect_url)
2028
2029    def test_double_login_is_not_allowed(self):
2030        """Regression test for #19327"""
2031        login_url = "%s?next=%s" % (reverse("admin:login"), reverse("admin:index"))
2032
2033        response = self.client.get(self.index_url)
2034        self.assertEqual(response.status_code, 302)
2035
2036        # Establish a valid admin session
2037        login = self.client.post(login_url, self.super_login)
2038        self.assertRedirects(login, self.index_url)
2039        self.assertFalse(login.context)
2040
2041        # Logging in with non-admin user fails
2042        login = self.client.post(login_url, self.joepublic_login)
2043        self.assertEqual(login.status_code, 200)
2044        self.assertContains(login, ERROR_MESSAGE)
2045
2046        # Establish a valid admin session
2047        login = self.client.post(login_url, self.super_login)
2048        self.assertRedirects(login, self.index_url)
2049        self.assertFalse(login.context)
2050
2051        # Logging in with admin user while already logged in
2052        login = self.client.post(login_url, self.super_login)
2053        self.assertRedirects(login, self.index_url)
2054        self.assertFalse(login.context)
2055        self.client.get(reverse("admin:logout"))
2056
2057    def test_login_page_notice_for_non_staff_users(self):
2058        """
2059        A logged-in non-staff user trying to access the admin index should be
2060        presented with the login page and a hint indicating that the current
2061        user doesn't have access to it.
2062        """
2063        hint_template = "You are authenticated as {}"
2064
2065        # Anonymous user should not be shown the hint
2066        response = self.client.get(self.index_url, follow=True)
2067        self.assertContains(response, "login-form")
2068        self.assertNotContains(response, hint_template.format(""), status_code=200)
2069
2070        # Non-staff user should be shown the hint
2071        self.client.force_login(self.nostaffuser)
2072        response = self.client.get(self.index_url, follow=True)
2073        self.assertContains(response, "login-form")
2074        self.assertContains(
2075            response, hint_template.format(self.nostaffuser.username), status_code=200
2076        )
2077
2078    def test_add_view(self):
2079        """Test add view restricts access and actually adds items."""
2080        add_dict = {
2081            "title": "Døm ikke",
2082            "content": "<p>great article</p>",
2083            "date_0": "2008-03-18",
2084            "date_1": "10:54:39",
2085            "section": self.s1.pk,
2086        }
2087        # Change User should not have access to add articles
2088        self.client.force_login(self.changeuser)
2089        # make sure the view removes test cookie
2090        self.assertIs(self.client.session.test_cookie_worked(), False)
2091        response = self.client.get(reverse("admin:admin_views_article_add"))
2092        self.assertEqual(response.status_code, 403)
2093        # Try POST just to make sure
2094        post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2095        self.assertEqual(post.status_code, 403)
2096        self.assertEqual(Article.objects.count(), 3)
2097        self.client.get(reverse("admin:logout"))
2098
2099        # View User should not have access to add articles
2100        self.client.force_login(self.viewuser)
2101        response = self.client.get(reverse("admin:admin_views_article_add"))
2102        self.assertEqual(response.status_code, 403)
2103        # Try POST just to make sure
2104        post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2105        self.assertEqual(post.status_code, 403)
2106        self.assertEqual(Article.objects.count(), 3)
2107        # Now give the user permission to add but not change.
2108        self.viewuser.user_permissions.add(
2109            get_perm(Article, get_permission_codename("add", Article._meta))
2110        )
2111        response = self.client.get(reverse("admin:admin_views_article_add"))
2112        self.assertContains(
2113            response, '<input type="submit" value="Save and view" name="_continue">'
2114        )
2115        post = self.client.post(
2116            reverse("admin:admin_views_article_add"), add_dict, follow=False
2117        )
2118        self.assertEqual(post.status_code, 302)
2119        self.assertEqual(Article.objects.count(), 4)
2120        article = Article.objects.latest("pk")
2121        response = self.client.get(
2122            reverse("admin:admin_views_article_change", args=(article.pk,))
2123        )
2124        self.assertContains(
2125            response,
2126            '<li class="success">The article “Døm ikke” was added successfully.</li>',
2127        )
2128        article.delete()
2129        self.client.get(reverse("admin:logout"))
2130
2131        # Add user may login and POST to add view, then redirect to admin root
2132        self.client.force_login(self.adduser)
2133        addpage = self.client.get(reverse("admin:admin_views_article_add"))
2134        change_list_link = '&rsaquo; <a href="%s">Articles</a>' % reverse(
2135            "admin:admin_views_article_changelist"
2136        )
2137        self.assertNotContains(
2138            addpage,
2139            change_list_link,
2140            msg_prefix="User restricted to add permission is given link to change list view in breadcrumbs.",
2141        )
2142        post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2143        self.assertRedirects(post, self.index_url)
2144        self.assertEqual(Article.objects.count(), 4)
2145        self.assertEqual(len(mail.outbox), 2)
2146        self.assertEqual(mail.outbox[0].subject, "Greetings from a created object")
2147        self.client.get(reverse("admin:logout"))
2148
2149        # The addition was logged correctly
2150        addition_log = LogEntry.objects.all()[0]
2151        new_article = Article.objects.last()
2152        article_ct = ContentType.objects.get_for_model(Article)
2153        self.assertEqual(addition_log.user_id, self.adduser.pk)
2154        self.assertEqual(addition_log.content_type_id, article_ct.pk)
2155        self.assertEqual(addition_log.object_id, str(new_article.pk))
2156        self.assertEqual(addition_log.object_repr, "Døm ikke")
2157        self.assertEqual(addition_log.action_flag, ADDITION)
2158        self.assertEqual(addition_log.get_change_message(), "Added.")
2159
2160        # Super can add too, but is redirected to the change list view
2161        self.client.force_login(self.superuser)
2162        addpage = self.client.get(reverse("admin:admin_views_article_add"))
2163        self.assertContains(
2164            addpage,
2165            change_list_link,
2166            msg_prefix="Unrestricted user is not given link to change list view in breadcrumbs.",
2167        )
2168        post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2169        self.assertRedirects(post, reverse("admin:admin_views_article_changelist"))
2170        self.assertEqual(Article.objects.count(), 5)
2171        self.client.get(reverse("admin:logout"))
2172
2173        # 8509 - if a normal user is already logged in, it is possible
2174        # to change user into the superuser without error
2175        self.client.force_login(self.joepublicuser)
2176        # Check and make sure that if user expires, data still persists
2177        self.client.force_login(self.superuser)
2178        # make sure the view removes test cookie
2179        self.assertIs(self.client.session.test_cookie_worked(), False)
2180
2181    @mock.patch("django.contrib.admin.options.InlineModelAdmin.has_change_permission")
2182    def test_add_view_with_view_only_inlines(self, has_change_permission):
2183        """User with add permission to a section but view-only for inlines."""
2184        self.viewuser.user_permissions.add(
2185            get_perm(Section, get_permission_codename("add", Section._meta))
2186        )
2187        self.client.force_login(self.viewuser)
2188        # Valid POST creates a new section.
2189        data = {
2190            "name": "New obj",
2191            "article_set-TOTAL_FORMS": 0,
2192            "article_set-INITIAL_FORMS": 0,
2193        }
2194        response = self.client.post(reverse("admin:admin_views_section_add"), data)
2195        self.assertRedirects(response, reverse("admin:index"))
2196        self.assertEqual(Section.objects.latest("id").name, data["name"])
2197        # InlineModelAdmin.has_change_permission()'s obj argument is always
2198        # None during object add.
2199        self.assertEqual(
2200            [obj for (request, obj), _ in has_change_permission.call_args_list],
2201            [None, None],
2202        )
2203
2204    def test_change_view(self):
2205        """Change view should restrict access and allow users to edit items."""
2206        change_dict = {
2207            "title": "Ikke fordømt",
2208            "content": "<p>edited article</p>",
2209            "date_0": "2008-03-18",
2210            "date_1": "10:54:39",
2211            "section": self.s1.pk,
2212        }
2213        article_change_url = reverse(
2214            "admin:admin_views_article_change", args=(self.a1.pk,)
2215        )
2216        article_changelist_url = reverse("admin:admin_views_article_changelist")
2217
2218        # add user should not be able to view the list of article or change any of them
2219        self.client.force_login(self.adduser)
2220        response = self.client.get(article_changelist_url)
2221        self.assertEqual(response.status_code, 403)
2222        response = self.client.get(article_change_url)
2223        self.assertEqual(response.status_code, 403)
2224        post = self.client.post(article_change_url, change_dict)
2225        self.assertEqual(post.status_code, 403)
2226        self.client.get(reverse("admin:logout"))
2227
2228        # view user can view articles but not make changes.
2229        self.client.force_login(self.viewuser)
2230        response = self.client.get(article_changelist_url)
2231        self.assertEqual(response.status_code, 200)
2232        self.assertEqual(response.context["title"], "Select article to view")
2233        response = self.client.get(article_change_url)
2234        self.assertEqual(response.status_code, 200)
2235        self.assertEqual(response.context["title"], "View article")
2236        self.assertContains(response, "<label>Extra form field:</label>")
2237        self.assertContains(
2238            response,
2239            '<a href="/test_admin/admin/admin_views/article/" class="closelink">Close</a>',
2240        )
2241        post = self.client.post(article_change_url, change_dict)
2242        self.assertEqual(post.status_code, 403)
2243        self.assertEqual(
2244            Article.objects.get(pk=self.a1.pk).content, "<p>Middle content</p>"
2245        )
2246        self.client.get(reverse("admin:logout"))
2247
2248        # change user can view all items and edit them
2249        self.client.force_login(self.changeuser)
2250        response = self.client.get(article_changelist_url)
2251        self.assertEqual(response.status_code, 200)
2252        self.assertEqual(response.context["title"], "Select article to change")
2253        response = self.client.get(article_change_url)
2254        self.assertEqual(response.status_code, 200)
2255        self.assertEqual(response.context["title"], "Change article")
2256        post = self.client.post(article_change_url, change_dict)
2257        self.assertRedirects(post, article_changelist_url)
2258        self.assertEqual(
2259            Article.objects.get(pk=self.a1.pk).content, "<p>edited article</p>"
2260        )
2261
2262        # one error in form should produce singular error message, multiple errors plural
2263        change_dict["title"] = ""
2264        post = self.client.post(article_change_url, change_dict)
2265        self.assertContains(
2266            post,
2267            "Please correct the error below.",
2268            msg_prefix="Singular error message not found in response to post with one error",
2269        )
2270
2271        change_dict["content"] = ""
2272        post = self.client.post(article_change_url, change_dict)
2273        self.assertContains(
2274            post,
2275            "Please correct the errors below.",
2276            msg_prefix="Plural error message not found in response to post with multiple errors",
2277        )
2278        self.client.get(reverse("admin:logout"))
2279
2280        # Test redirection when using row-level change permissions. Refs #11513.
2281        r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
2282        r2 = RowLevelChangePermissionModel.objects.create(id=2, name="even id")
2283        r3 = RowLevelChangePermissionModel.objects.create(id=3, name="odd id mult 3")
2284        r6 = RowLevelChangePermissionModel.objects.create(id=6, name="even id mult 3")
2285        change_url_1 = reverse(
2286            "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r1.pk,)
2287        )
2288        change_url_2 = reverse(
2289            "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r2.pk,)
2290        )
2291        change_url_3 = reverse(
2292            "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r3.pk,)
2293        )
2294        change_url_6 = reverse(
2295            "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r6.pk,)
2296        )
2297        logins = [
2298            self.superuser,
2299            self.viewuser,
2300            self.adduser,
2301            self.changeuser,
2302            self.deleteuser,
2303        ]
2304        for login_user in logins:
2305            with self.subTest(login_user.username):
2306                self.client.force_login(login_user)
2307                response = self.client.get(change_url_1)
2308                self.assertEqual(response.status_code, 403)
2309                response = self.client.post(change_url_1, {"name": "changed"})
2310                self.assertEqual(
2311                    RowLevelChangePermissionModel.objects.get(id=1).name, "odd id"
2312                )
2313                self.assertEqual(response.status_code, 403)
2314                response = self.client.get(change_url_2)
2315                self.assertEqual(response.status_code, 200)
2316                response = self.client.post(change_url_2, {"name": "changed"})
2317                self.assertEqual(
2318                    RowLevelChangePermissionModel.objects.get(id=2).name, "changed"
2319                )
2320                self.assertRedirects(response, self.index_url)
2321                response = self.client.get(change_url_3)
2322                self.assertEqual(response.status_code, 200)
2323                response = self.client.post(change_url_3, {"name": "changed"})
2324                self.assertEqual(response.status_code, 403)
2325                self.assertEqual(
2326                    RowLevelChangePermissionModel.objects.get(id=3).name,
2327                    "odd id mult 3",
2328                )
2329                response = self.client.get(change_url_6)
2330                self.assertEqual(response.status_code, 200)
2331                response = self.client.post(change_url_6, {"name": "changed"})
2332                self.assertEqual(
2333                    RowLevelChangePermissionModel.objects.get(id=6).name, "changed"
2334                )
2335                self.assertRedirects(response, self.index_url)
2336
2337                self.client.get(reverse("admin:logout"))
2338
2339        for login_user in [self.joepublicuser, self.nostaffuser]:
2340            with self.subTest(login_user.username):
2341                self.client.force_login(login_user)
2342                response = self.client.get(change_url_1, follow=True)
2343                self.assertContains(response, "login-form")
2344                response = self.client.post(
2345                    change_url_1, {"name": "changed"}, follow=True
2346                )
2347                self.assertEqual(
2348                    RowLevelChangePermissionModel.objects.get(id=1).name, "odd id"
2349                )
2350                self.assertContains(response, "login-form")
2351                response = self.client.get(change_url_2, follow=True)
2352                self.assertContains(response, "login-form")
2353                response = self.client.post(
2354                    change_url_2, {"name": "changed again"}, follow=True
2355                )
2356                self.assertEqual(
2357                    RowLevelChangePermissionModel.objects.get(id=2).name, "changed"
2358                )
2359                self.assertContains(response, "login-form")
2360                self.client.get(reverse("admin:logout"))
2361
2362    def test_change_view_without_object_change_permission(self):
2363        """
2364        The object should be read-only if the user has permission to view it
2365        and change objects of that type but not to change the current object.
2366        """
2367        change_url = reverse("admin9:admin_views_article_change", args=(self.a1.pk,))
2368        self.client.force_login(self.viewuser)
2369        response = self.client.get(change_url)
2370        self.assertEqual(response.status_code, 200)
2371        self.assertEqual(response.context["title"], "View article")
2372        self.assertContains(
2373            response,
2374            '<a href="/test_admin/admin9/admin_views/article/" class="closelink">Close</a>',
2375        )
2376
2377    def test_change_view_save_as_new(self):
2378        """
2379        'Save as new' should raise PermissionDenied for users without the 'add'
2380        permission.
2381        """
2382        change_dict_save_as_new = {
2383            "_saveasnew": "Save as new",
2384            "title": "Ikke fordømt",
2385            "content": "<p>edited article</p>",
2386            "date_0": "2008-03-18",
2387            "date_1": "10:54:39",
2388            "section": self.s1.pk,
2389        }
2390        article_change_url = reverse(
2391            "admin:admin_views_article_change", args=(self.a1.pk,)
2392        )
2393
2394        # Add user can perform "Save as new".
2395        article_count = Article.objects.count()
2396        self.client.force_login(self.adduser)
2397        post = self.client.post(article_change_url, change_dict_save_as_new)
2398        self.assertRedirects(post, self.index_url)
2399        self.assertEqual(Article.objects.count(), article_count + 1)
2400        self.client.logout()
2401
2402        # Change user cannot perform "Save as new" (no 'add' permission).
2403        article_count = Article.objects.count()
2404        self.client.force_login(self.changeuser)
2405        post = self.client.post(article_change_url, change_dict_save_as_new)
2406        self.assertEqual(post.status_code, 403)
2407        self.assertEqual(Article.objects.count(), article_count)
2408
2409        # User with both add and change permissions should be redirected to the
2410        # change page for the newly created object.
2411        article_count = Article.objects.count()
2412        self.client.force_login(self.superuser)
2413        post = self.client.post(article_change_url, change_dict_save_as_new)
2414        self.assertEqual(Article.objects.count(), article_count + 1)
2415        new_article = Article.objects.latest("id")
2416        self.assertRedirects(
2417            post, reverse("admin:admin_views_article_change", args=(new_article.pk,))
2418        )
2419
2420    def test_change_view_with_view_only_inlines(self):
2421        """
2422        User with change permission to a section but view-only for inlines.
2423        """
2424        self.viewuser.user_permissions.add(
2425            get_perm(Section, get_permission_codename("change", Section._meta))
2426        )
2427        self.client.force_login(self.viewuser)
2428        # GET shows inlines.
2429        response = self.client.get(
2430            reverse("admin:admin_views_section_change", args=(self.s1.pk,))
2431        )
2432        self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2433        formset = response.context["inline_admin_formsets"][0]
2434        self.assertEqual(len(formset.forms), 3)
2435        # Valid POST changes the name.
2436        data = {
2437            "name": "Can edit name with view-only inlines",
2438            "article_set-TOTAL_FORMS": 3,
2439            "article_set-INITIAL_FORMS": 3,
2440        }
2441        response = self.client.post(
2442            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2443        )
2444        self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2445        self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data["name"])
2446        # Invalid POST reshows inlines.
2447        del data["name"]
2448        response = self.client.post(
2449            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2450        )
2451        self.assertEqual(response.status_code, 200)
2452        self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2453        formset = response.context["inline_admin_formsets"][0]
2454        self.assertEqual(len(formset.forms), 3)
2455
2456    def test_change_view_with_view_and_add_inlines(self):
2457        """User has view and add permissions on the inline model."""
2458        self.viewuser.user_permissions.add(
2459            get_perm(Section, get_permission_codename("change", Section._meta))
2460        )
2461        self.viewuser.user_permissions.add(
2462            get_perm(Article, get_permission_codename("add", Article._meta))
2463        )
2464        self.client.force_login(self.viewuser)
2465        # GET shows inlines.
2466        response = self.client.get(
2467            reverse("admin:admin_views_section_change", args=(self.s1.pk,))
2468        )
2469        self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2470        formset = response.context["inline_admin_formsets"][0]
2471        self.assertEqual(len(formset.forms), 6)
2472        # Valid POST creates a new article.
2473        data = {
2474            "name": "Can edit name with view-only inlines",
2475            "article_set-TOTAL_FORMS": 6,
2476            "article_set-INITIAL_FORMS": 3,
2477            "article_set-3-id": [""],
2478            "article_set-3-title": ["A title"],
2479            "article_set-3-content": ["Added content"],
2480            "article_set-3-date_0": ["2008-3-18"],
2481            "article_set-3-date_1": ["11:54:58"],
2482            "article_set-3-section": [str(self.s1.pk)],
2483        }
2484        response = self.client.post(
2485            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2486        )
2487        self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2488        self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data["name"])
2489        self.assertEqual(Article.objects.count(), 4)
2490        # Invalid POST reshows inlines.
2491        del data["name"]
2492        response = self.client.post(
2493            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2494        )
2495        self.assertEqual(response.status_code, 200)
2496        self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2497        formset = response.context["inline_admin_formsets"][0]
2498        self.assertEqual(len(formset.forms), 6)
2499
2500    def test_change_view_with_view_and_delete_inlines(self):
2501        """User has view and delete permissions on the inline model."""
2502        self.viewuser.user_permissions.add(
2503            get_perm(Section, get_permission_codename("change", Section._meta))
2504        )
2505        self.client.force_login(self.viewuser)
2506        data = {
2507            "name": "Name is required.",
2508            "article_set-TOTAL_FORMS": 6,
2509            "article_set-INITIAL_FORMS": 3,
2510            "article_set-0-id": [str(self.a1.pk)],
2511            "article_set-0-DELETE": ["on"],
2512        }
2513        # Inline POST details are ignored without delete permission.
2514        response = self.client.post(
2515            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2516        )
2517        self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2518        self.assertEqual(Article.objects.count(), 3)
2519        # Deletion successful when delete permission is added.
2520        self.viewuser.user_permissions.add(
2521            get_perm(Article, get_permission_codename("delete", Article._meta))
2522        )
2523        data = {
2524            "name": "Name is required.",
2525            "article_set-TOTAL_FORMS": 6,
2526            "article_set-INITIAL_FORMS": 3,
2527            "article_set-0-id": [str(self.a1.pk)],
2528            "article_set-0-DELETE": ["on"],
2529        }
2530        response = self.client.post(
2531            reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2532        )
2533        self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2534        self.assertEqual(Article.objects.count(), 2)
2535
2536    def test_delete_view(self):
2537        """Delete view should restrict access and actually delete items."""
2538        delete_dict = {"post": "yes"}
2539        delete_url = reverse("admin:admin_views_article_delete", args=(self.a1.pk,))
2540
2541        # add user should not be able to delete articles
2542        self.client.force_login(self.adduser)
2543        response = self.client.get(delete_url)
2544        self.assertEqual(response.status_code, 403)
2545        post = self.client.post(delete_url, delete_dict)
2546        self.assertEqual(post.status_code, 403)
2547        self.assertEqual(Article.objects.count(), 3)
2548        self.client.logout()
2549
2550        # view user should not be able to delete articles
2551        self.client.force_login(self.viewuser)
2552        response = self.client.get(delete_url)
2553        self.assertEqual(response.status_code, 403)
2554        post = self.client.post(delete_url, delete_dict)
2555        self.assertEqual(post.status_code, 403)
2556        self.assertEqual(Article.objects.count(), 3)
2557        self.client.logout()
2558
2559        # Delete user can delete
2560        self.client.force_login(self.deleteuser)
2561        response = self.client.get(
2562            reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
2563        )
2564        self.assertContains(response, "<h2>Summary</h2>")
2565        self.assertContains(response, "<li>Articles: 3</li>")
2566        # test response contains link to related Article
2567        self.assertContains(response, "admin_views/article/%s/" % self.a1.pk)
2568
2569        response = self.client.get(delete_url)
2570        self.assertContains(response, "admin_views/article/%s/" % self.a1.pk)
2571        self.assertContains(response, "<h2>Summary</h2>")
2572        self.assertContains(response, "<li>Articles: 1</li>")
2573        self.assertEqual(response.status_code, 200)
2574        post = self.client.post(delete_url, delete_dict)
2575        self.assertRedirects(post, self.index_url)
2576        self.assertEqual(Article.objects.count(), 2)
2577        self.assertEqual(len(mail.outbox), 1)
2578        self.assertEqual(mail.outbox[0].subject, "Greetings from a deleted object")
2579        article_ct = ContentType.objects.get_for_model(Article)
2580        logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION)
2581        self.assertEqual(logged.object_id, str(self.a1.pk))
2582
2583    def test_delete_view_with_no_default_permissions(self):
2584        """
2585        The delete view allows users to delete collected objects without a
2586        'delete' permission (ReadOnlyPizza.Meta.default_permissions is empty).
2587        """
2588        pizza = ReadOnlyPizza.objects.create(name="Double Cheese")
2589        delete_url = reverse("admin:admin_views_readonlypizza_delete", args=(pizza.pk,))
2590        self.client.force_login(self.adduser)
2591        response = self.client.get(delete_url)
2592        self.assertContains(response, "admin_views/readonlypizza/%s/" % pizza.pk)
2593        self.assertContains(response, "<h2>Summary</h2>")
2594        self.assertContains(response, "<li>Read only pizzas: 1</li>")
2595        self.assertEqual(response.status_code, 200)
2596        post = self.client.post(delete_url, {"post": "yes"})
2597        self.assertRedirects(
2598            post, reverse("admin:admin_views_readonlypizza_changelist")
2599        )
2600        self.assertEqual(ReadOnlyPizza.objects.count(), 0)
2601
2602    def test_delete_view_nonexistent_obj(self):
2603        self.client.force_login(self.deleteuser)
2604        url = reverse("admin:admin_views_article_delete", args=("nonexistent",))
2605        response = self.client.get(url, follow=True)
2606        self.assertRedirects(response, reverse("admin:index"))
2607        self.assertEqual(
2608            [m.message for m in response.context["messages"]],
2609            ["article with ID “nonexistent” doesn’t exist. Perhaps it was deleted?"],
2610        )
2611
2612    def test_history_view(self):
2613        """History view should restrict access."""
2614        # add user should not be able to view the list of article or change any of them
2615        self.client.force_login(self.adduser)
2616        response = self.client.get(
2617            reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2618        )
2619        self.assertEqual(response.status_code, 403)
2620        self.client.get(reverse("admin:logout"))
2621
2622        # view user can view all items
2623        self.client.force_login(self.viewuser)
2624        response = self.client.get(
2625            reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2626        )
2627        self.assertEqual(response.status_code, 200)
2628        self.client.get(reverse("admin:logout"))
2629
2630        # change user can view all items and edit them
2631        self.client.force_login(self.changeuser)
2632        response = self.client.get(
2633            reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2634        )
2635        self.assertEqual(response.status_code, 200)
2636
2637        # Test redirection when using row-level change permissions. Refs #11513.
2638        rl1 = RowLevelChangePermissionModel.objects.create(name="odd id")
2639        rl2 = RowLevelChangePermissionModel.objects.create(name="even id")
2640        logins = [
2641            self.superuser,
2642            self.viewuser,
2643            self.adduser,
2644            self.changeuser,
2645            self.deleteuser,
2646        ]
2647        for login_user in logins:
2648            with self.subTest(login_user.username):
2649                self.client.force_login(login_user)
2650                url = reverse(
2651                    "admin:admin_views_rowlevelchangepermissionmodel_history",
2652                    args=(rl1.pk,),
2653                )
2654                response = self.client.get(url)
2655                self.assertEqual(response.status_code, 403)
2656
2657                url = reverse(
2658                    "admin:admin_views_rowlevelchangepermissionmodel_history",
2659                    args=(rl2.pk,),
2660                )
2661                response = self.client.get(url)
2662                self.assertEqual(response.status_code, 200)
2663
2664                self.client.get(reverse("admin:logout"))
2665
2666        for login_user in [self.joepublicuser, self.nostaffuser]:
2667            with self.subTest(login_user.username):
2668                self.client.force_login(login_user)
2669                url = reverse(
2670                    "admin:admin_views_rowlevelchangepermissionmodel_history",
2671                    args=(rl1.pk,),
2672                )
2673                response = self.client.get(url, follow=True)
2674                self.assertContains(response, "login-form")
2675                url = reverse(
2676                    "admin:admin_views_rowlevelchangepermissionmodel_history",
2677                    args=(rl2.pk,),
2678                )
2679                response = self.client.get(url, follow=True)
2680                self.assertContains(response, "login-form")
2681
2682                self.client.get(reverse("admin:logout"))
2683
2684    def test_history_view_bad_url(self):
2685        self.client.force_login(self.changeuser)
2686        response = self.client.get(
2687            reverse("admin:admin_views_article_history", args=("foo",)), follow=True
2688        )
2689        self.assertRedirects(response, reverse("admin:index"))
2690        self.assertEqual(
2691            [m.message for m in response.context["messages"]],
2692            ["article with ID “foo” doesn’t exist. Perhaps it was deleted?"],
2693        )
2694
2695    def test_conditionally_show_add_section_link(self):
2696        """
2697        The foreign key widget should only show the "add related" button if the
2698        user has permission to add that related item.
2699        """
2700        self.client.force_login(self.adduser)
2701        # The user can't add sections yet, so they shouldn't see the "add section" link.
2702        url = reverse("admin:admin_views_article_add")
2703        add_link_text = "add_id_section"
2704        response = self.client.get(url)
2705        self.assertNotContains(response, add_link_text)
2706        # Allow the user to add sections too. Now they can see the "add section" link.
2707        user = User.objects.get(username="adduser")
2708        perm = get_perm(Section, get_permission_codename("add", Section._meta))
2709        user.user_permissions.add(perm)
2710        response = self.client.get(url)
2711        self.assertContains(response, add_link_text)
2712
2713    def test_conditionally_show_change_section_link(self):
2714        """
2715        The foreign key widget should only show the "change related" button if
2716        the user has permission to change that related item.
2717        """
2718
2719        def get_change_related(response):
2720            return (
2721                response.context["adminform"]
2722                .form.fields["section"]
2723                .widget.can_change_related
2724            )
2725
2726        self.client.force_login(self.adduser)
2727        # The user can't change sections yet, so they shouldn't see the "change section" link.
2728        url = reverse("admin:admin_views_article_add")
2729        change_link_text = "change_id_section"
2730        response = self.client.get(url)
2731        self.assertFalse(get_change_related(response))
2732        self.assertNotContains(response, change_link_text)
2733        # Allow the user to change sections too. Now they can see the "change section" link.
2734        user = User.objects.get(username="adduser")
2735        perm = get_perm(Section, get_permission_codename("change", Section._meta))
2736        user.user_permissions.add(perm)
2737        response = self.client.get(url)
2738        self.assertTrue(get_change_related(response))
2739        self.assertContains(response, change_link_text)
2740
2741    def test_conditionally_show_delete_section_link(self):
2742        """
2743        The foreign key widget should only show the "delete related" button if
2744        the user has permission to delete that related item.
2745        """
2746
2747        def get_delete_related(response):
2748            return (
2749                response.context["adminform"]
2750                .form.fields["sub_section"]
2751                .widget.can_delete_related
2752            )
2753
2754        self.client.force_login(self.adduser)
2755        # The user can't delete sections yet, so they shouldn't see the "delete section" link.
2756        url = reverse("admin:admin_views_article_add")
2757        delete_link_text = "delete_id_sub_section"
2758        response = self.client.get(url)
2759        self.assertFalse(get_delete_related(response))
2760        self.assertNotContains(response, delete_link_text)
2761        # Allow the user to delete sections too. Now they can see the "delete section" link.
2762        user = User.objects.get(username="adduser")
2763        perm = get_perm(Section, get_permission_codename("delete", Section._meta))
2764        user.user_permissions.add(perm)
2765        response = self.client.get(url)
2766        self.assertTrue(get_delete_related(response))
2767        self.assertContains(response, delete_link_text)
2768
2769    def test_disabled_permissions_when_logged_in(self):
2770        self.client.force_login(self.superuser)
2771        superuser = User.objects.get(username="super")
2772        superuser.is_active = False
2773        superuser.save()
2774
2775        response = self.client.get(self.index_url, follow=True)
2776        self.assertContains(response, 'id="login-form"')
2777        self.assertNotContains(response, "Log out")
2778
2779        response = self.client.get(reverse("secure_view"), follow=True)
2780        self.assertContains(response, 'id="login-form"')
2781
2782    def test_disabled_staff_permissions_when_logged_in(self):
2783        self.client.force_login(self.superuser)
2784        superuser = User.objects.get(username="super")
2785        superuser.is_staff = False
2786        superuser.save()
2787
2788        response = self.client.get(self.index_url, follow=True)
2789        self.assertContains(response, 'id="login-form"')
2790        self.assertNotContains(response, "Log out")
2791
2792        response = self.client.get(reverse("secure_view"), follow=True)
2793        self.assertContains(response, 'id="login-form"')
2794
2795    def test_app_list_permissions(self):
2796        """
2797        If a user has no module perms, the app list returns a 404.
2798        """
2799        opts = Article._meta
2800        change_user = User.objects.get(username="changeuser")
2801        permission = get_perm(Article, get_permission_codename("change", opts))
2802
2803        self.client.force_login(self.changeuser)
2804
2805        # the user has no module permissions
2806        change_user.user_permissions.remove(permission)
2807        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
2808        self.assertEqual(response.status_code, 404)
2809
2810        # the user now has module permissions
2811        change_user.user_permissions.add(permission)
2812        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
2813        self.assertEqual(response.status_code, 200)
2814
2815    def test_shortcut_view_only_available_to_staff(self):
2816        """
2817        Only admin users should be able to use the admin shortcut view.
2818        """
2819        model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey)
2820        obj = ModelWithStringPrimaryKey.objects.create(string_pk="foo")
2821        shortcut_url = reverse("admin:view_on_site", args=(model_ctype.pk, obj.pk))
2822
2823        # Not logged in: we should see the login page.
2824        response = self.client.get(shortcut_url, follow=True)
2825        self.assertTemplateUsed(response, "admin/login.html")
2826
2827        # Logged in? Redirect.
2828        self.client.force_login(self.superuser)
2829        response = self.client.get(shortcut_url, follow=False)
2830        # Can't use self.assertRedirects() because User.get_absolute_url() is silly.
2831        self.assertEqual(response.status_code, 302)
2832        # Domain may depend on contrib.sites tests also run
2833        self.assertRegex(response.url, "http://(testserver|example.com)/dummy/foo/")
2834
2835    def test_has_module_permission(self):
2836        """
2837        has_module_permission() returns True for all users who
2838        have any permission for that module (add, change, or delete), so that
2839        the module is displayed on the admin index page.
2840        """
2841        self.client.force_login(self.superuser)
2842        response = self.client.get(self.index_url)
2843        self.assertContains(response, "admin_views")
2844        self.assertContains(response, "Articles")
2845        self.client.logout()
2846
2847        self.client.force_login(self.viewuser)
2848        response = self.client.get(self.index_url)
2849        self.assertContains(response, "admin_views")
2850        self.assertContains(response, "Articles")
2851        self.client.logout()
2852
2853        self.client.force_login(self.adduser)
2854        response = self.client.get(self.index_url)
2855        self.assertContains(response, "admin_views")
2856        self.assertContains(response, "Articles")
2857        self.client.logout()
2858
2859        self.client.force_login(self.changeuser)
2860        response = self.client.get(self.index_url)
2861        self.assertContains(response, "admin_views")
2862        self.assertContains(response, "Articles")
2863        self.client.logout()
2864
2865        self.client.force_login(self.deleteuser)
2866        response = self.client.get(self.index_url)
2867        self.assertContains(response, "admin_views")
2868        self.assertContains(response, "Articles")
2869
2870    def test_overriding_has_module_permission(self):
2871        """
2872        If has_module_permission() always returns False, the module shouldn't
2873        be displayed on the admin index page for any users.
2874        """
2875        articles = Article._meta.verbose_name_plural.title()
2876        sections = Section._meta.verbose_name_plural.title()
2877        index_url = reverse("admin7:index")
2878
2879        self.client.force_login(self.superuser)
2880        response = self.client.get(index_url)
2881        self.assertContains(response, sections)
2882        self.assertNotContains(response, articles)
2883        self.client.logout()
2884
2885        self.client.force_login(self.viewuser)
2886        response = self.client.get(index_url)
2887        self.assertNotContains(response, "admin_views")
2888        self.assertNotContains(response, articles)
2889        self.client.logout()
2890
2891        self.client.force_login(self.adduser)
2892        response = self.client.get(index_url)
2893        self.assertNotContains(response, "admin_views")
2894        self.assertNotContains(response, articles)
2895        self.client.logout()
2896
2897        self.client.force_login(self.changeuser)
2898        response = self.client.get(index_url)
2899        self.assertNotContains(response, "admin_views")
2900        self.assertNotContains(response, articles)
2901        self.client.logout()
2902
2903        self.client.force_login(self.deleteuser)
2904        response = self.client.get(index_url)
2905        self.assertNotContains(response, articles)
2906
2907        # The app list displays Sections but not Articles as the latter has
2908        # ModelAdmin.has_module_permission() = False.
2909        self.client.force_login(self.superuser)
2910        response = self.client.get(reverse("admin7:app_list", args=("admin_views",)))
2911        self.assertContains(response, sections)
2912        self.assertNotContains(response, articles)
2913
2914    def test_post_save_message_no_forbidden_links_visible(self):
2915        """
2916        Post-save message shouldn't contain a link to the change form if the
2917        user doesn't have the change permission.
2918        """
2919        self.client.force_login(self.adduser)
2920        # Emulate Article creation for user with add-only permission.
2921        post_data = {
2922            "title": "Fun & games",
2923            "content": "Some content",
2924            "date_0": "2015-10-31",
2925            "date_1": "16:35:00",
2926            "_save": "Save",
2927        }
2928        response = self.client.post(
2929            reverse("admin:admin_views_article_add"), post_data, follow=True
2930        )
2931        self.assertContains(
2932            response,
2933            '<li class="success">The article “Fun &amp; games” was added successfully.</li>',
2934            html=True,
2935        )
2936
2937
2938@override_settings(
2939    ROOT_URLCONF="admin_views.urls",
2940    TEMPLATES=[
2941        {
2942            "BACKEND": "django.template.backends.django.DjangoTemplates",
2943            "APP_DIRS": True,
2944            "OPTIONS": {
2945                "context_processors": [
2946                    "django.contrib.auth.context_processors.auth",
2947                    "django.contrib.messages.context_processors.messages",
2948                ],
2949            },
2950        }
2951    ],
2952)
2953class AdminViewProxyModelPermissionsTests(TestCase):
2954    """Tests for proxy models permissions in the admin."""
2955
2956    @classmethod
2957    def setUpTestData(cls):
2958        cls.viewuser = User.objects.create_user(
2959            username="viewuser", password="secret", is_staff=True
2960        )
2961        cls.adduser = User.objects.create_user(
2962            username="adduser", password="secret", is_staff=True
2963        )
2964        cls.changeuser = User.objects.create_user(
2965            username="changeuser", password="secret", is_staff=True
2966        )
2967        cls.deleteuser = User.objects.create_user(
2968            username="deleteuser", password="secret", is_staff=True
2969        )
2970        # Setup permissions.
2971        opts = UserProxy._meta
2972        cls.viewuser.user_permissions.add(
2973            get_perm(UserProxy, get_permission_codename("view", opts))
2974        )
2975        cls.adduser.user_permissions.add(
2976            get_perm(UserProxy, get_permission_codename("add", opts))
2977        )
2978        cls.changeuser.user_permissions.add(
2979            get_perm(UserProxy, get_permission_codename("change", opts))
2980        )
2981        cls.deleteuser.user_permissions.add(
2982            get_perm(UserProxy, get_permission_codename("delete", opts))
2983        )
2984        # UserProxy instances.
2985        cls.user_proxy = UserProxy.objects.create(
2986            username="user_proxy", password="secret"
2987        )
2988
2989    def test_add(self):
2990        self.client.force_login(self.adduser)
2991        url = reverse("admin:admin_views_userproxy_add")
2992        data = {
2993            "username": "can_add",
2994            "password": "secret",
2995            "date_joined_0": "2019-01-15",
2996            "date_joined_1": "16:59:10",
2997        }
2998        response = self.client.post(url, data, follow=True)
2999        self.assertEqual(response.status_code, 200)
3000        self.assertTrue(UserProxy.objects.filter(username="can_add").exists())
3001
3002    def test_view(self):
3003        self.client.force_login(self.viewuser)
3004        response = self.client.get(reverse("admin:admin_views_userproxy_changelist"))
3005        self.assertContains(response, "<h1>Select user proxy to view</h1>")
3006        self.assertEqual(response.status_code, 200)
3007        response = self.client.get(
3008            reverse("admin:admin_views_userproxy_change", args=(self.user_proxy.pk,))
3009        )
3010        self.assertContains(response, "<h1>View user proxy</h1>")
3011        self.assertContains(response, '<div class="readonly">user_proxy</div>')
3012
3013    def test_change(self):
3014        self.client.force_login(self.changeuser)
3015        data = {
3016            "password": self.user_proxy.password,
3017            "username": self.user_proxy.username,
3018            "date_joined_0": self.user_proxy.date_joined.strftime("%Y-%m-%d"),
3019            "date_joined_1": self.user_proxy.date_joined.strftime("%H:%M:%S"),
3020            "first_name": "first_name",
3021        }
3022        url = reverse("admin:admin_views_userproxy_change", args=(self.user_proxy.pk,))
3023        response = self.client.post(url, data)
3024        self.assertRedirects(
3025            response, reverse("admin:admin_views_userproxy_changelist")
3026        )
3027        self.assertEqual(
3028            UserProxy.objects.get(pk=self.user_proxy.pk).first_name, "first_name"
3029        )
3030
3031    def test_delete(self):
3032        self.client.force_login(self.deleteuser)
3033        url = reverse("admin:admin_views_userproxy_delete", args=(self.user_proxy.pk,))
3034        response = self.client.post(url, {"post": "yes"}, follow=True)
3035        self.assertEqual(response.status_code, 200)
3036        self.assertFalse(UserProxy.objects.filter(pk=self.user_proxy.pk).exists())
3037
3038
3039@override_settings(ROOT_URLCONF="admin_views.urls")
3040class AdminViewsNoUrlTest(TestCase):
3041    """Regression test for #17333"""
3042
3043    @classmethod
3044    def setUpTestData(cls):
3045        # User who can change Reports
3046        cls.changeuser = User.objects.create_user(
3047            username="changeuser", password="secret", is_staff=True
3048        )
3049        cls.changeuser.user_permissions.add(
3050            get_perm(Report, get_permission_codename("change", Report._meta))
3051        )
3052
3053    def test_no_standard_modeladmin_urls(self):
3054        """Admin index views don't break when user's ModelAdmin removes standard urls"""
3055        self.client.force_login(self.changeuser)
3056        r = self.client.get(reverse("admin:index"))
3057        # we shouldn't get a 500 error caused by a NoReverseMatch
3058        self.assertEqual(r.status_code, 200)
3059        self.client.get(reverse("admin:logout"))
3060
3061
3062@skipUnlessDBFeature("can_defer_constraint_checks")
3063@override_settings(ROOT_URLCONF="admin_views.urls")
3064class AdminViewDeletedObjectsTest(TestCase):
3065    @classmethod
3066    def setUpTestData(cls):
3067        cls.superuser = User.objects.create_superuser(
3068            username="super", password="secret", email="super@example.com"
3069        )
3070        cls.deleteuser = User.objects.create_user(
3071            username="deleteuser", password="secret", is_staff=True
3072        )
3073        cls.s1 = Section.objects.create(name="Test section")
3074        cls.a1 = Article.objects.create(
3075            content="<p>Middle content</p>",
3076            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3077            section=cls.s1,
3078        )
3079        cls.a2 = Article.objects.create(
3080            content="<p>Oldest content</p>",
3081            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3082            section=cls.s1,
3083        )
3084        cls.a3 = Article.objects.create(
3085            content="<p>Newest content</p>",
3086            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3087            section=cls.s1,
3088        )
3089        cls.p1 = PrePopulatedPost.objects.create(
3090            title="A Long Title", published=True, slug="a-long-title"
3091        )
3092
3093        cls.v1 = Villain.objects.create(name="Adam")
3094        cls.v2 = Villain.objects.create(name="Sue")
3095        cls.sv1 = SuperVillain.objects.create(name="Bob")
3096        cls.pl1 = Plot.objects.create(
3097            name="World Domination", team_leader=cls.v1, contact=cls.v2
3098        )
3099        cls.pl2 = Plot.objects.create(
3100            name="World Peace", team_leader=cls.v2, contact=cls.v2
3101        )
3102        cls.pl3 = Plot.objects.create(
3103            name="Corn Conspiracy", team_leader=cls.v1, contact=cls.v1
3104        )
3105        cls.pd1 = PlotDetails.objects.create(details="almost finished", plot=cls.pl1)
3106        cls.sh1 = SecretHideout.objects.create(
3107            location="underground bunker", villain=cls.v1
3108        )
3109        cls.sh2 = SecretHideout.objects.create(
3110            location="floating castle", villain=cls.sv1
3111        )
3112        cls.ssh1 = SuperSecretHideout.objects.create(
3113            location="super floating castle!", supervillain=cls.sv1
3114        )
3115        cls.cy1 = CyclicOne.objects.create(name="I am recursive", two_id=1)
3116        cls.cy2 = CyclicTwo.objects.create(name="I am recursive too", one_id=1)
3117
3118    def setUp(self):
3119        self.client.force_login(self.superuser)
3120
3121    def test_nesting(self):
3122        """
3123        Objects should be nested to display the relationships that
3124        cause them to be scheduled for deletion.
3125        """
3126        pattern = re.compile(
3127            r'<li>Plot: <a href="%s">World Domination</a>\s*<ul>\s*'
3128            r'<li>Plot details: <a href="%s">almost finished</a>'
3129            % (
3130                reverse("admin:admin_views_plot_change", args=(self.pl1.pk,)),
3131                reverse("admin:admin_views_plotdetails_change", args=(self.pd1.pk,)),
3132            )
3133        )
3134        response = self.client.get(
3135            reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3136        )
3137        self.assertRegex(response.content.decode(), pattern)
3138
3139    def test_cyclic(self):
3140        """
3141        Cyclic relationships should still cause each object to only be
3142        listed once.
3143        """
3144        one = '<li>Cyclic one: <a href="%s">I am recursive</a>' % (
3145            reverse("admin:admin_views_cyclicone_change", args=(self.cy1.pk,)),
3146        )
3147        two = '<li>Cyclic two: <a href="%s">I am recursive too</a>' % (
3148            reverse("admin:admin_views_cyclictwo_change", args=(self.cy2.pk,)),
3149        )
3150        response = self.client.get(
3151            reverse("admin:admin_views_cyclicone_delete", args=(self.cy1.pk,))
3152        )
3153
3154        self.assertContains(response, one, 1)
3155        self.assertContains(response, two, 1)
3156
3157    def test_perms_needed(self):
3158        self.client.logout()
3159        delete_user = User.objects.get(username="deleteuser")
3160        delete_user.user_permissions.add(
3161            get_perm(Plot, get_permission_codename("delete", Plot._meta))
3162        )
3163
3164        self.client.force_login(self.deleteuser)
3165        response = self.client.get(
3166            reverse("admin:admin_views_plot_delete", args=(self.pl1.pk,))
3167        )
3168        self.assertContains(
3169            response,
3170            "your account doesn't have permission to delete the following types of objects",
3171        )
3172        self.assertContains(response, "<li>plot details</li>")
3173
3174    def test_protected(self):
3175        q = Question.objects.create(question="Why?")
3176        a1 = Answer.objects.create(question=q, answer="Because.")
3177        a2 = Answer.objects.create(question=q, answer="Yes.")
3178
3179        response = self.client.get(
3180            reverse("admin:admin_views_question_delete", args=(q.pk,))
3181        )
3182        self.assertContains(
3183            response, "would require deleting the following protected related objects"
3184        )
3185        self.assertContains(
3186            response,
3187            '<li>Answer: <a href="%s">Because.</a></li>'
3188            % reverse("admin:admin_views_answer_change", args=(a1.pk,)),
3189        )
3190        self.assertContains(
3191            response,
3192            '<li>Answer: <a href="%s">Yes.</a></li>'
3193            % reverse("admin:admin_views_answer_change", args=(a2.pk,)),
3194        )
3195
3196    def test_post_delete_protected(self):
3197        """
3198        A POST request to delete protected objects should display the page
3199        which says the deletion is prohibited.
3200        """
3201        q = Question.objects.create(question="Why?")
3202        Answer.objects.create(question=q, answer="Because.")
3203
3204        response = self.client.post(
3205            reverse("admin:admin_views_question_delete", args=(q.pk,)), {"post": "yes"}
3206        )
3207        self.assertEqual(Question.objects.count(), 1)
3208        self.assertContains(
3209            response, "would require deleting the following protected related objects"
3210        )
3211
3212    def test_restricted(self):
3213        album = Album.objects.create(title="Amaryllis")
3214        song = Song.objects.create(album=album, name="Unity")
3215        response = self.client.get(
3216            reverse("admin:admin_views_album_delete", args=(album.pk,))
3217        )
3218        self.assertContains(
3219            response, "would require deleting the following protected related objects",
3220        )
3221        self.assertContains(
3222            response,
3223            '<li>Song: <a href="%s">Unity</a></li>'
3224            % reverse("admin:admin_views_song_change", args=(song.pk,)),
3225        )
3226
3227    def test_post_delete_restricted(self):
3228        album = Album.objects.create(title="Amaryllis")
3229        Song.objects.create(album=album, name="Unity")
3230        response = self.client.post(
3231            reverse("admin:admin_views_album_delete", args=(album.pk,)),
3232            {"post": "yes"},
3233        )
3234        self.assertEqual(Album.objects.count(), 1)
3235        self.assertContains(
3236            response, "would require deleting the following protected related objects",
3237        )
3238
3239    def test_not_registered(self):
3240        should_contain = """<li>Secret hideout: underground bunker"""
3241        response = self.client.get(
3242            reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3243        )
3244        self.assertContains(response, should_contain, 1)
3245
3246    def test_multiple_fkeys_to_same_model(self):
3247        """
3248        If a deleted object has two relationships from another model,
3249        both of those should be followed in looking for related
3250        objects to delete.
3251        """
3252        should_contain = '<li>Plot: <a href="%s">World Domination</a>' % reverse(
3253            "admin:admin_views_plot_change", args=(self.pl1.pk,)
3254        )
3255        response = self.client.get(
3256            reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3257        )
3258        self.assertContains(response, should_contain)
3259        response = self.client.get(
3260            reverse("admin:admin_views_villain_delete", args=(self.v2.pk,))
3261        )
3262        self.assertContains(response, should_contain)
3263
3264    def test_multiple_fkeys_to_same_instance(self):
3265        """
3266        If a deleted object has two relationships pointing to it from
3267        another object, the other object should still only be listed
3268        once.
3269        """
3270        should_contain = '<li>Plot: <a href="%s">World Peace</a></li>' % reverse(
3271            "admin:admin_views_plot_change", args=(self.pl2.pk,)
3272        )
3273        response = self.client.get(
3274            reverse("admin:admin_views_villain_delete", args=(self.v2.pk,))
3275        )
3276        self.assertContains(response, should_contain, 1)
3277
3278    def test_inheritance(self):
3279        """
3280        In the case of an inherited model, if either the child or
3281        parent-model instance is deleted, both instances are listed
3282        for deletion, as well as any relationships they have.
3283        """
3284        should_contain = [
3285            '<li>Villain: <a href="%s">Bob</a>'
3286            % reverse("admin:admin_views_villain_change", args=(self.sv1.pk,)),
3287            '<li>Super villain: <a href="%s">Bob</a>'
3288            % reverse("admin:admin_views_supervillain_change", args=(self.sv1.pk,)),
3289            "<li>Secret hideout: floating castle",
3290            "<li>Super secret hideout: super floating castle!",
3291        ]
3292        response = self.client.get(
3293            reverse("admin:admin_views_villain_delete", args=(self.sv1.pk,))
3294        )
3295        for should in should_contain:
3296            self.assertContains(response, should, 1)
3297        response = self.client.get(
3298            reverse("admin:admin_views_supervillain_delete", args=(self.sv1.pk,))
3299        )
3300        for should in should_contain:
3301            self.assertContains(response, should, 1)
3302
3303    def test_generic_relations(self):
3304        """
3305        If a deleted object has GenericForeignKeys pointing to it,
3306        those objects should be listed for deletion.
3307        """
3308        plot = self.pl3
3309        tag = FunkyTag.objects.create(content_object=plot, name="hott")
3310        should_contain = '<li>Funky tag: <a href="%s">hott' % reverse(
3311            "admin:admin_views_funkytag_change", args=(tag.id,)
3312        )
3313        response = self.client.get(
3314            reverse("admin:admin_views_plot_delete", args=(plot.pk,))
3315        )
3316        self.assertContains(response, should_contain)
3317
3318    def test_generic_relations_with_related_query_name(self):
3319        """
3320        If a deleted object has GenericForeignKey with
3321        GenericRelation(related_query_name='...') pointing to it, those objects
3322        should be listed for deletion.
3323        """
3324        bookmark = Bookmark.objects.create(name="djangoproject")
3325        tag = FunkyTag.objects.create(content_object=bookmark, name="django")
3326        tag_url = reverse("admin:admin_views_funkytag_change", args=(tag.id,))
3327        should_contain = '<li>Funky tag: <a href="%s">django' % tag_url
3328        response = self.client.get(
3329            reverse("admin:admin_views_bookmark_delete", args=(bookmark.pk,))
3330        )
3331        self.assertContains(response, should_contain)
3332
3333    def test_delete_view_uses_get_deleted_objects(self):
3334        """The delete view uses ModelAdmin.get_deleted_objects()."""
3335        book = Book.objects.create(name="Test Book")
3336        response = self.client.get(
3337            reverse("admin2:admin_views_book_delete", args=(book.pk,))
3338        )
3339        # BookAdmin.get_deleted_objects() returns custom text.
3340        self.assertContains(response, "a deletable object")
3341
3342
3343@override_settings(ROOT_URLCONF="admin_views.urls")
3344class TestGenericRelations(TestCase):
3345    @classmethod
3346    def setUpTestData(cls):
3347        cls.superuser = User.objects.create_superuser(
3348            username="super", password="secret", email="super@example.com"
3349        )
3350        cls.v1 = Villain.objects.create(name="Adam")
3351        cls.pl3 = Plot.objects.create(
3352            name="Corn Conspiracy", team_leader=cls.v1, contact=cls.v1
3353        )
3354
3355    def setUp(self):
3356        self.client.force_login(self.superuser)
3357
3358    def test_generic_content_object_in_list_display(self):
3359        FunkyTag.objects.create(content_object=self.pl3, name="hott")
3360        response = self.client.get(reverse("admin:admin_views_funkytag_changelist"))
3361        self.assertContains(response, "%s</td>" % self.pl3)
3362
3363
3364@override_settings(ROOT_URLCONF="admin_views.urls")
3365class AdminViewStringPrimaryKeyTest(TestCase):
3366    @classmethod
3367    def setUpTestData(cls):
3368        cls.superuser = User.objects.create_superuser(
3369            username="super", password="secret", email="super@example.com"
3370        )
3371        cls.s1 = Section.objects.create(name="Test section")
3372        cls.a1 = Article.objects.create(
3373            content="<p>Middle content</p>",
3374            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3375            section=cls.s1,
3376        )
3377        cls.a2 = Article.objects.create(
3378            content="<p>Oldest content</p>",
3379            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3380            section=cls.s1,
3381        )
3382        cls.a3 = Article.objects.create(
3383            content="<p>Newest content</p>",
3384            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3385            section=cls.s1,
3386        )
3387        cls.p1 = PrePopulatedPost.objects.create(
3388            title="A Long Title", published=True, slug="a-long-title"
3389        )
3390        cls.pk = (
3391            "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 "
3392            r"""-_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`"""
3393        )
3394        cls.m1 = ModelWithStringPrimaryKey.objects.create(string_pk=cls.pk)
3395        content_type_pk = ContentType.objects.get_for_model(
3396            ModelWithStringPrimaryKey
3397        ).pk
3398        user_pk = cls.superuser.pk
3399        LogEntry.objects.log_action(
3400            user_pk,
3401            content_type_pk,
3402            cls.pk,
3403            cls.pk,
3404            2,
3405            change_message="Changed something",
3406        )
3407
3408    def setUp(self):
3409        self.client.force_login(self.superuser)
3410
3411    def test_get_history_view(self):
3412        """
3413        Retrieving the history for an object using urlencoded form of primary
3414        key should work.
3415        Refs #12349, #18550.
3416        """
3417        response = self.client.get(
3418            reverse(
3419                "admin:admin_views_modelwithstringprimarykey_history", args=(self.pk,)
3420            )
3421        )
3422        self.assertContains(response, escape(self.pk))
3423        self.assertContains(response, "Changed something")
3424        self.assertEqual(response.status_code, 200)
3425
3426    def test_get_change_view(self):
3427        "Retrieving the object using urlencoded form of primary key should work"
3428        response = self.client.get(
3429            reverse(
3430                "admin:admin_views_modelwithstringprimarykey_change", args=(self.pk,)
3431            )
3432        )
3433        self.assertContains(response, escape(self.pk))
3434        self.assertEqual(response.status_code, 200)
3435
3436    def test_changelist_to_changeform_link(self):
3437        "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072"
3438        response = self.client.get(
3439            reverse("admin:admin_views_modelwithstringprimarykey_changelist")
3440        )
3441        # this URL now comes through reverse(), thus url quoting and iri_to_uri encoding
3442        pk_final_url = escape(iri_to_uri(quote(self.pk)))
3443        change_url = reverse(
3444            "admin:admin_views_modelwithstringprimarykey_change", args=("__fk__",)
3445        ).replace("__fk__", pk_final_url)
3446        should_contain = '<th class="field-__str__"><a href="%s">%s</a></th>' % (
3447            change_url,
3448            escape(self.pk),
3449        )
3450        self.assertContains(response, should_contain)
3451
3452    def test_recentactions_link(self):
3453        "The link from the recent actions list referring to the changeform of the object should be quoted"
3454        response = self.client.get(reverse("admin:index"))
3455        link = reverse(
3456            "admin:admin_views_modelwithstringprimarykey_change", args=(quote(self.pk),)
3457        )
3458        should_contain = """<a href="%s">%s</a>""" % (escape(link), escape(self.pk))
3459        self.assertContains(response, should_contain)
3460
3461    def test_deleteconfirmation_link(self):
3462        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
3463        url = reverse(
3464            "admin:admin_views_modelwithstringprimarykey_delete", args=(quote(self.pk),)
3465        )
3466        response = self.client.get(url)
3467        # this URL now comes through reverse(), thus url quoting and iri_to_uri encoding
3468        change_url = reverse(
3469            "admin:admin_views_modelwithstringprimarykey_change", args=("__fk__",)
3470        ).replace("__fk__", escape(iri_to_uri(quote(self.pk))))
3471        should_contain = '<a href="%s">%s</a>' % (change_url, escape(self.pk))
3472        self.assertContains(response, should_contain)
3473
3474    def test_url_conflicts_with_add(self):
3475        "A model with a primary key that ends with add or is `add` should be visible"
3476        add_model = ModelWithStringPrimaryKey.objects.create(
3477            pk="i have something to add"
3478        )
3479        add_model.save()
3480        response = self.client.get(
3481            reverse(
3482                "admin:admin_views_modelwithstringprimarykey_change",
3483                args=(quote(add_model.pk),),
3484            )
3485        )
3486        should_contain = """<h1>Change model with string primary key</h1>"""
3487        self.assertContains(response, should_contain)
3488
3489        add_model2 = ModelWithStringPrimaryKey.objects.create(pk="add")
3490        add_url = reverse("admin:admin_views_modelwithstringprimarykey_add")
3491        change_url = reverse(
3492            "admin:admin_views_modelwithstringprimarykey_change",
3493            args=(quote(add_model2.pk),),
3494        )
3495        self.assertNotEqual(add_url, change_url)
3496
3497    def test_url_conflicts_with_delete(self):
3498        "A model with a primary key that ends with delete should be visible"
3499        delete_model = ModelWithStringPrimaryKey(pk="delete")
3500        delete_model.save()
3501        response = self.client.get(
3502            reverse(
3503                "admin:admin_views_modelwithstringprimarykey_change",
3504                args=(quote(delete_model.pk),),
3505            )
3506        )
3507        should_contain = """<h1>Change model with string primary key</h1>"""
3508        self.assertContains(response, should_contain)
3509
3510    def test_url_conflicts_with_history(self):
3511        "A model with a primary key that ends with history should be visible"
3512        history_model = ModelWithStringPrimaryKey(pk="history")
3513        history_model.save()
3514        response = self.client.get(
3515            reverse(
3516                "admin:admin_views_modelwithstringprimarykey_change",
3517                args=(quote(history_model.pk),),
3518            )
3519        )
3520        should_contain = """<h1>Change model with string primary key</h1>"""
3521        self.assertContains(response, should_contain)
3522
3523    def test_shortcut_view_with_escaping(self):
3524        "'View on site should' work properly with char fields"
3525        model = ModelWithStringPrimaryKey(pk="abc_123")
3526        model.save()
3527        response = self.client.get(
3528            reverse(
3529                "admin:admin_views_modelwithstringprimarykey_change",
3530                args=(quote(model.pk),),
3531            )
3532        )
3533        should_contain = '/%s/" class="viewsitelink">' % model.pk
3534        self.assertContains(response, should_contain)
3535
3536    def test_change_view_history_link(self):
3537        """Object history button link should work and contain the pk value quoted."""
3538        url = reverse(
3539            "admin:%s_modelwithstringprimarykey_change"
3540            % ModelWithStringPrimaryKey._meta.app_label,
3541            args=(quote(self.pk),),
3542        )
3543        response = self.client.get(url)
3544        self.assertEqual(response.status_code, 200)
3545        expected_link = reverse(
3546            "admin:%s_modelwithstringprimarykey_history"
3547            % ModelWithStringPrimaryKey._meta.app_label,
3548            args=(quote(self.pk),),
3549        )
3550        self.assertContains(
3551            response, '<a href="%s" class="historylink"' % escape(expected_link)
3552        )
3553
3554    def test_redirect_on_add_view_continue_button(self):
3555        """As soon as an object is added using "Save and continue editing"
3556        button, the user should be redirected to the object's change_view.
3557
3558        In case primary key is a string containing some special characters
3559        like slash or underscore, these characters must be escaped (see #22266)
3560        """
3561        response = self.client.post(
3562            reverse("admin:admin_views_modelwithstringprimarykey_add"),
3563            {
3564                "string_pk": "123/history",
3565                "_continue": "1",  # Save and continue editing
3566            },
3567        )
3568
3569        self.assertEqual(response.status_code, 302)  # temporary redirect
3570        self.assertIn("/123_2Fhistory/", response["location"])  # PK is quoted
3571
3572
3573@override_settings(ROOT_URLCONF="admin_views.urls")
3574class SecureViewTests(TestCase):
3575    """
3576    Test behavior of a view protected by the staff_member_required decorator.
3577    """
3578
3579    def test_secure_view_shows_login_if_not_logged_in(self):
3580        secure_url = reverse("secure_view")
3581        response = self.client.get(secure_url)
3582        self.assertRedirects(
3583            response, "%s?next=%s" % (reverse("admin:login"), secure_url)
3584        )
3585        response = self.client.get(secure_url, follow=True)
3586        self.assertTemplateUsed(response, "admin/login.html")
3587        self.assertEqual(response.context[REDIRECT_FIELD_NAME], secure_url)
3588
3589    def test_staff_member_required_decorator_works_with_argument(self):
3590        """
3591        Staff_member_required decorator works with an argument
3592        (redirect_field_name).
3593        """
3594        secure_url = "/test_admin/admin/secure-view2/"
3595        response = self.client.get(secure_url)
3596        self.assertRedirects(
3597            response, "%s?myfield=%s" % (reverse("admin:login"), secure_url)
3598        )
3599
3600
3601@override_settings(ROOT_URLCONF="admin_views.urls")
3602class AdminViewUnicodeTest(TestCase):
3603    @classmethod
3604    def setUpTestData(cls):
3605        cls.superuser = User.objects.create_superuser(
3606            username="super", password="secret", email="super@example.com"
3607        )
3608        cls.b1 = Book.objects.create(name="Lærdommer")
3609        cls.p1 = Promo.objects.create(name="<Promo for Lærdommer>", book=cls.b1)
3610        cls.chap1 = Chapter.objects.create(
3611            title="Norske bostaver æøå skaper problemer",
3612            content="<p>Svært frustrerende med UnicodeDecodeErro</p>",
3613            book=cls.b1,
3614        )
3615        cls.chap2 = Chapter.objects.create(
3616            title="Kjærlighet",
3617            content="<p>La kjærligheten til de lidende seire.</p>",
3618            book=cls.b1,
3619        )
3620        cls.chap3 = Chapter.objects.create(
3621            title="Kjærlighet", content="<p>Noe innhold</p>", book=cls.b1
3622        )
3623        cls.chap4 = ChapterXtra1.objects.create(
3624            chap=cls.chap1, xtra="<Xtra(1) Norske bostaver æøå skaper problemer>"
3625        )
3626        cls.chap5 = ChapterXtra1.objects.create(
3627            chap=cls.chap2, xtra="<Xtra(1) Kjærlighet>"
3628        )
3629        cls.chap6 = ChapterXtra1.objects.create(
3630            chap=cls.chap3, xtra="<Xtra(1) Kjærlighet>"
3631        )
3632        cls.chap7 = ChapterXtra2.objects.create(
3633            chap=cls.chap1, xtra="<Xtra(2) Norske bostaver æøå skaper problemer>"
3634        )
3635        cls.chap8 = ChapterXtra2.objects.create(
3636            chap=cls.chap2, xtra="<Xtra(2) Kjærlighet>"
3637        )
3638        cls.chap9 = ChapterXtra2.objects.create(
3639            chap=cls.chap3, xtra="<Xtra(2) Kjærlighet>"
3640        )
3641
3642    def setUp(self):
3643        self.client.force_login(self.superuser)
3644
3645    def test_unicode_edit(self):
3646        """
3647        A test to ensure that POST on edit_view handles non-ASCII characters.
3648        """
3649        post_data = {
3650            "name": "Test lærdommer",
3651            # inline data
3652            "chapter_set-TOTAL_FORMS": "6",
3653            "chapter_set-INITIAL_FORMS": "3",
3654            "chapter_set-MAX_NUM_FORMS": "0",
3655            "chapter_set-0-id": self.chap1.pk,
3656            "chapter_set-0-title": "Norske bostaver æøå skaper problemer",
3657            "chapter_set-0-content": "&lt;p&gt;Svært frustrerende med UnicodeDecodeError&lt;/p&gt;",
3658            "chapter_set-1-id": self.chap2.id,
3659            "chapter_set-1-title": "Kjærlighet.",
3660            "chapter_set-1-content": "&lt;p&gt;La kjærligheten til de lidende seire.&lt;/p&gt;",
3661            "chapter_set-2-id": self.chap3.id,
3662            "chapter_set-2-title": "Need a title.",
3663            "chapter_set-2-content": "&lt;p&gt;Newest content&lt;/p&gt;",
3664            "chapter_set-3-id": "",
3665            "chapter_set-3-title": "",
3666            "chapter_set-3-content": "",
3667            "chapter_set-4-id": "",
3668            "chapter_set-4-title": "",
3669            "chapter_set-4-content": "",
3670            "chapter_set-5-id": "",
3671            "chapter_set-5-title": "",
3672            "chapter_set-5-content": "",
3673        }
3674
3675        response = self.client.post(
3676            reverse("admin:admin_views_book_change", args=(self.b1.pk,)), post_data
3677        )
3678        self.assertEqual(response.status_code, 302)  # redirect somewhere
3679
3680    def test_unicode_delete(self):
3681        """
3682        The delete_view handles non-ASCII characters
3683        """
3684        delete_dict = {"post": "yes"}
3685        delete_url = reverse("admin:admin_views_book_delete", args=(self.b1.pk,))
3686        response = self.client.get(delete_url)
3687        self.assertEqual(response.status_code, 200)
3688        response = self.client.post(delete_url, delete_dict)
3689        self.assertRedirects(response, reverse("admin:admin_views_book_changelist"))
3690
3691
3692@override_settings(ROOT_URLCONF="admin_views.urls")
3693class AdminViewListEditable(TestCase):
3694    @classmethod
3695    def setUpTestData(cls):
3696        cls.superuser = User.objects.create_superuser(
3697            username="super", password="secret", email="super@example.com"
3698        )
3699        cls.s1 = Section.objects.create(name="Test section")
3700        cls.a1 = Article.objects.create(
3701            content="<p>Middle content</p>",
3702            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3703            section=cls.s1,
3704        )
3705        cls.a2 = Article.objects.create(
3706            content="<p>Oldest content</p>",
3707            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3708            section=cls.s1,
3709        )
3710        cls.a3 = Article.objects.create(
3711            content="<p>Newest content</p>",
3712            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3713            section=cls.s1,
3714        )
3715        cls.p1 = PrePopulatedPost.objects.create(
3716            title="A Long Title", published=True, slug="a-long-title"
3717        )
3718        cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
3719        cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
3720        cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
3721
3722    def setUp(self):
3723        self.client.force_login(self.superuser)
3724
3725    def test_inheritance(self):
3726        Podcast.objects.create(
3727            name="This Week in Django", release_date=datetime.date.today()
3728        )
3729        response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
3730        self.assertEqual(response.status_code, 200)
3731
3732    def test_inheritance_2(self):
3733        Vodcast.objects.create(name="This Week in Django", released=True)
3734        response = self.client.get(reverse("admin:admin_views_vodcast_changelist"))
3735        self.assertEqual(response.status_code, 200)
3736
3737    def test_custom_pk(self):
3738        Language.objects.create(iso="en", name="English", english_name="English")
3739        response = self.client.get(reverse("admin:admin_views_language_changelist"))
3740        self.assertEqual(response.status_code, 200)
3741
3742    def test_changelist_input_html(self):
3743        response = self.client.get(reverse("admin:admin_views_person_changelist"))
3744        # 2 inputs per object(the field and the hidden id field) = 6
3745        # 4 management hidden fields = 4
3746        # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
3747        # main form submit button = 1
3748        # search field and search submit button = 2
3749        # CSRF field = 1
3750        # field to track 'select all' across paginated views = 1
3751        # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
3752        self.assertContains(response, "<input", count=19)
3753        # 1 select per object = 3 selects
3754        self.assertContains(response, "<select", count=4)
3755
3756    def test_post_messages(self):
3757        # Ticket 12707: Saving inline editable should not show admin
3758        # action warnings
3759        data = {
3760            "form-TOTAL_FORMS": "3",
3761            "form-INITIAL_FORMS": "3",
3762            "form-MAX_NUM_FORMS": "0",
3763            "form-0-gender": "1",
3764            "form-0-id": "%s" % self.per1.pk,
3765            "form-1-gender": "2",
3766            "form-1-id": "%s" % self.per2.pk,
3767            "form-2-alive": "checked",
3768            "form-2-gender": "1",
3769            "form-2-id": "%s" % self.per3.pk,
3770            "_save": "Save",
3771        }
3772        response = self.client.post(
3773            reverse("admin:admin_views_person_changelist"), data, follow=True
3774        )
3775        self.assertEqual(len(response.context["messages"]), 1)
3776
3777    def test_post_submission(self):
3778        data = {
3779            "form-TOTAL_FORMS": "3",
3780            "form-INITIAL_FORMS": "3",
3781            "form-MAX_NUM_FORMS": "0",
3782            "form-0-gender": "1",
3783            "form-0-id": "%s" % self.per1.pk,
3784            "form-1-gender": "2",
3785            "form-1-id": "%s" % self.per2.pk,
3786            "form-2-alive": "checked",
3787            "form-2-gender": "1",
3788            "form-2-id": "%s" % self.per3.pk,
3789            "_save": "Save",
3790        }
3791        self.client.post(reverse("admin:admin_views_person_changelist"), data)
3792
3793        self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
3794        self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
3795
3796        # test a filtered page
3797        data = {
3798            "form-TOTAL_FORMS": "2",
3799            "form-INITIAL_FORMS": "2",
3800            "form-MAX_NUM_FORMS": "0",
3801            "form-0-id": "%s" % self.per1.pk,
3802            "form-0-gender": "1",
3803            "form-0-alive": "checked",
3804            "form-1-id": "%s" % self.per3.pk,
3805            "form-1-gender": "1",
3806            "form-1-alive": "checked",
3807            "_save": "Save",
3808        }
3809        self.client.post(
3810            reverse("admin:admin_views_person_changelist") + "?gender__exact=1", data
3811        )
3812
3813        self.assertIs(Person.objects.get(name="John Mauchly").alive, True)
3814
3815        # test a searched page
3816        data = {
3817            "form-TOTAL_FORMS": "1",
3818            "form-INITIAL_FORMS": "1",
3819            "form-MAX_NUM_FORMS": "0",
3820            "form-0-id": "%s" % self.per1.pk,
3821            "form-0-gender": "1",
3822            "_save": "Save",
3823        }
3824        self.client.post(
3825            reverse("admin:admin_views_person_changelist") + "?q=john", data
3826        )
3827
3828        self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
3829
3830    def test_non_field_errors(self):
3831        """
3832        Non-field errors are displayed for each of the forms in the
3833        changelist's formset.
3834        """
3835        fd1 = FoodDelivery.objects.create(
3836            reference="123", driver="bill", restaurant="thai"
3837        )
3838        fd2 = FoodDelivery.objects.create(
3839            reference="456", driver="bill", restaurant="india"
3840        )
3841        fd3 = FoodDelivery.objects.create(
3842            reference="789", driver="bill", restaurant="pizza"
3843        )
3844
3845        data = {
3846            "form-TOTAL_FORMS": "3",
3847            "form-INITIAL_FORMS": "3",
3848            "form-MAX_NUM_FORMS": "0",
3849            "form-0-id": str(fd1.id),
3850            "form-0-reference": "123",
3851            "form-0-driver": "bill",
3852            "form-0-restaurant": "thai",
3853            # Same data as above: Forbidden because of unique_together!
3854            "form-1-id": str(fd2.id),
3855            "form-1-reference": "456",
3856            "form-1-driver": "bill",
3857            "form-1-restaurant": "thai",
3858            "form-2-id": str(fd3.id),
3859            "form-2-reference": "789",
3860            "form-2-driver": "bill",
3861            "form-2-restaurant": "pizza",
3862            "_save": "Save",
3863        }
3864        response = self.client.post(
3865            reverse("admin:admin_views_fooddelivery_changelist"), data
3866        )
3867        self.assertContains(
3868            response,
3869            '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery '
3870            "with this Driver and Restaurant already exists.</li></ul></td></tr>",
3871            1,
3872            html=True,
3873        )
3874
3875        data = {
3876            "form-TOTAL_FORMS": "3",
3877            "form-INITIAL_FORMS": "3",
3878            "form-MAX_NUM_FORMS": "0",
3879            "form-0-id": str(fd1.id),
3880            "form-0-reference": "123",
3881            "form-0-driver": "bill",
3882            "form-0-restaurant": "thai",
3883            # Same data as above: Forbidden because of unique_together!
3884            "form-1-id": str(fd2.id),
3885            "form-1-reference": "456",
3886            "form-1-driver": "bill",
3887            "form-1-restaurant": "thai",
3888            # Same data also.
3889            "form-2-id": str(fd3.id),
3890            "form-2-reference": "789",
3891            "form-2-driver": "bill",
3892            "form-2-restaurant": "thai",
3893            "_save": "Save",
3894        }
3895        response = self.client.post(
3896            reverse("admin:admin_views_fooddelivery_changelist"), data
3897        )
3898        self.assertContains(
3899            response,
3900            '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery '
3901            "with this Driver and Restaurant already exists.</li></ul></td></tr>",
3902            2,
3903            html=True,
3904        )
3905
3906    def test_non_form_errors(self):
3907        # test if non-form errors are handled; ticket #12716
3908        data = {
3909            "form-TOTAL_FORMS": "1",
3910            "form-INITIAL_FORMS": "1",
3911            "form-MAX_NUM_FORMS": "0",
3912            "form-0-id": "%s" % self.per2.pk,
3913            "form-0-alive": "1",
3914            "form-0-gender": "2",
3915            # The form processing understands this as a list_editable "Save"
3916            # and not an action "Go".
3917            "_save": "Save",
3918        }
3919        response = self.client.post(
3920            reverse("admin:admin_views_person_changelist"), data
3921        )
3922        self.assertContains(response, "Grace is not a Zombie")
3923
3924    def test_non_form_errors_is_errorlist(self):
3925        # test if non-form errors are correctly handled; ticket #12878
3926        data = {
3927            "form-TOTAL_FORMS": "1",
3928            "form-INITIAL_FORMS": "1",
3929            "form-MAX_NUM_FORMS": "0",
3930            "form-0-id": "%s" % self.per2.pk,
3931            "form-0-alive": "1",
3932            "form-0-gender": "2",
3933            "_save": "Save",
3934        }
3935        response = self.client.post(
3936            reverse("admin:admin_views_person_changelist"), data
3937        )
3938        non_form_errors = response.context["cl"].formset.non_form_errors()
3939        self.assertIsInstance(non_form_errors, ErrorList)
3940        self.assertEqual(
3941            str(non_form_errors), str(ErrorList(["Grace is not a Zombie"]))
3942        )
3943
3944    def test_list_editable_ordering(self):
3945        collector = Collector.objects.create(id=1, name="Frederick Clegg")
3946
3947        Category.objects.create(id=1, order=1, collector=collector)
3948        Category.objects.create(id=2, order=2, collector=collector)
3949        Category.objects.create(id=3, order=0, collector=collector)
3950        Category.objects.create(id=4, order=0, collector=collector)
3951
3952        # NB: The order values must be changed so that the items are reordered.
3953        data = {
3954            "form-TOTAL_FORMS": "4",
3955            "form-INITIAL_FORMS": "4",
3956            "form-MAX_NUM_FORMS": "0",
3957            "form-0-order": "14",
3958            "form-0-id": "1",
3959            "form-0-collector": "1",
3960            "form-1-order": "13",
3961            "form-1-id": "2",
3962            "form-1-collector": "1",
3963            "form-2-order": "1",
3964            "form-2-id": "3",
3965            "form-2-collector": "1",
3966            "form-3-order": "0",
3967            "form-3-id": "4",
3968            "form-3-collector": "1",
3969            # The form processing understands this as a list_editable "Save"
3970            # and not an action "Go".
3971            "_save": "Save",
3972        }
3973        response = self.client.post(
3974            reverse("admin:admin_views_category_changelist"), data
3975        )
3976        # Successful post will redirect
3977        self.assertEqual(response.status_code, 302)
3978
3979        # The order values have been applied to the right objects
3980        self.assertEqual(Category.objects.get(id=1).order, 14)
3981        self.assertEqual(Category.objects.get(id=2).order, 13)
3982        self.assertEqual(Category.objects.get(id=3).order, 1)
3983        self.assertEqual(Category.objects.get(id=4).order, 0)
3984
3985    def test_list_editable_pagination(self):
3986        """
3987        Pagination works for list_editable items.
3988        """
3989        UnorderedObject.objects.create(id=1, name="Unordered object #1")
3990        UnorderedObject.objects.create(id=2, name="Unordered object #2")
3991        UnorderedObject.objects.create(id=3, name="Unordered object #3")
3992        response = self.client.get(
3993            reverse("admin:admin_views_unorderedobject_changelist")
3994        )
3995        self.assertContains(response, "Unordered object #3")
3996        self.assertContains(response, "Unordered object #2")
3997        self.assertNotContains(response, "Unordered object #1")
3998        response = self.client.get(
3999            reverse("admin:admin_views_unorderedobject_changelist") + "?p=1"
4000        )
4001        self.assertNotContains(response, "Unordered object #3")
4002        self.assertNotContains(response, "Unordered object #2")
4003        self.assertContains(response, "Unordered object #1")
4004
4005    def test_list_editable_action_submit(self):
4006        # List editable changes should not be executed if the action "Go" button is
4007        # used to submit the form.
4008        data = {
4009            "form-TOTAL_FORMS": "3",
4010            "form-INITIAL_FORMS": "3",
4011            "form-MAX_NUM_FORMS": "0",
4012            "form-0-gender": "1",
4013            "form-0-id": "1",
4014            "form-1-gender": "2",
4015            "form-1-id": "2",
4016            "form-2-alive": "checked",
4017            "form-2-gender": "1",
4018            "form-2-id": "3",
4019            "index": "0",
4020            "_selected_action": ["3"],
4021            "action": ["", "delete_selected"],
4022        }
4023        self.client.post(reverse("admin:admin_views_person_changelist"), data)
4024
4025        self.assertIs(Person.objects.get(name="John Mauchly").alive, True)
4026        self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 1)
4027
4028    def test_list_editable_action_choices(self):
4029        # List editable changes should be executed if the "Save" button is
4030        # used to submit the form - any action choices should be ignored.
4031        data = {
4032            "form-TOTAL_FORMS": "3",
4033            "form-INITIAL_FORMS": "3",
4034            "form-MAX_NUM_FORMS": "0",
4035            "form-0-gender": "1",
4036            "form-0-id": "%s" % self.per1.pk,
4037            "form-1-gender": "2",
4038            "form-1-id": "%s" % self.per2.pk,
4039            "form-2-alive": "checked",
4040            "form-2-gender": "1",
4041            "form-2-id": "%s" % self.per3.pk,
4042            "_save": "Save",
4043            "_selected_action": ["1"],
4044            "action": ["", "delete_selected"],
4045        }
4046        self.client.post(reverse("admin:admin_views_person_changelist"), data)
4047
4048        self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
4049        self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
4050
4051    def test_list_editable_popup(self):
4052        """
4053        Fields should not be list-editable in popups.
4054        """
4055        response = self.client.get(reverse("admin:admin_views_person_changelist"))
4056        self.assertNotEqual(response.context["cl"].list_editable, ())
4057        response = self.client.get(
4058            reverse("admin:admin_views_person_changelist") + "?%s" % IS_POPUP_VAR
4059        )
4060        self.assertEqual(response.context["cl"].list_editable, ())
4061
4062    def test_pk_hidden_fields(self):
4063        """
4064        hidden pk fields aren't displayed in the table body and their
4065        corresponding human-readable value is displayed instead. The hidden pk
4066        fields are displayed but separately (not in the table) and only once.
4067        """
4068        story1 = Story.objects.create(
4069            title="The adventures of Guido", content="Once upon a time in Djangoland..."
4070        )
4071        story2 = Story.objects.create(
4072            title="Crouching Tiger, Hidden Python",
4073            content="The Python was sneaking into...",
4074        )
4075        response = self.client.get(reverse("admin:admin_views_story_changelist"))
4076        # Only one hidden field, in a separate place than the table.
4077        self.assertContains(response, 'id="id_form-0-id"', 1)
4078        self.assertContains(response, 'id="id_form-1-id"', 1)
4079        self.assertContains(
4080            response,
4081            '<div class="hiddenfields">\n'
4082            '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
4083            '<input type="hidden" name="form-1-id" value="%d" id="id_form-1-id">\n</div>'
4084            % (story2.id, story1.id),
4085            html=True,
4086        )
4087        self.assertContains(response, '<td class="field-id">%d</td>' % story1.id, 1)
4088        self.assertContains(response, '<td class="field-id">%d</td>' % story2.id, 1)
4089
4090    def test_pk_hidden_fields_with_list_display_links(self):
4091        """ Similarly as test_pk_hidden_fields, but when the hidden pk fields are
4092            referenced in list_display_links.
4093            Refs #12475.
4094        """
4095        story1 = OtherStory.objects.create(
4096            title="The adventures of Guido",
4097            content="Once upon a time in Djangoland...",
4098        )
4099        story2 = OtherStory.objects.create(
4100            title="Crouching Tiger, Hidden Python",
4101            content="The Python was sneaking into...",
4102        )
4103        link1 = reverse("admin:admin_views_otherstory_change", args=(story1.pk,))
4104        link2 = reverse("admin:admin_views_otherstory_change", args=(story2.pk,))
4105        response = self.client.get(reverse("admin:admin_views_otherstory_changelist"))
4106        # Only one hidden field, in a separate place than the table.
4107        self.assertContains(response, 'id="id_form-0-id"', 1)
4108        self.assertContains(response, 'id="id_form-1-id"', 1)
4109        self.assertContains(
4110            response,
4111            '<div class="hiddenfields">\n'
4112            '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
4113            '<input type="hidden" name="form-1-id" value="%d" id="id_form-1-id">\n</div>'
4114            % (story2.id, story1.id),
4115            html=True,
4116        )
4117        self.assertContains(
4118            response,
4119            '<th class="field-id"><a href="%s">%d</a></th>' % (link1, story1.id),
4120            1,
4121        )
4122        self.assertContains(
4123            response,
4124            '<th class="field-id"><a href="%s">%d</a></th>' % (link2, story2.id),
4125            1,
4126        )
4127
4128
4129@override_settings(ROOT_URLCONF="admin_views.urls")
4130class AdminSearchTest(TestCase):
4131    @classmethod
4132    def setUpTestData(cls):
4133        cls.superuser = User.objects.create_superuser(
4134            username="super", password="secret", email="super@example.com"
4135        )
4136        cls.joepublicuser = User.objects.create_user(
4137            username="joepublic", password="secret"
4138        )
4139        cls.s1 = Section.objects.create(name="Test section")
4140        cls.a1 = Article.objects.create(
4141            content="<p>Middle content</p>",
4142            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
4143            section=cls.s1,
4144        )
4145        cls.a2 = Article.objects.create(
4146            content="<p>Oldest content</p>",
4147            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
4148            section=cls.s1,
4149        )
4150        cls.a3 = Article.objects.create(
4151            content="<p>Newest content</p>",
4152            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
4153            section=cls.s1,
4154        )
4155        cls.p1 = PrePopulatedPost.objects.create(
4156            title="A Long Title", published=True, slug="a-long-title"
4157        )
4158
4159        cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
4160        cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
4161        cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
4162
4163        cls.t1 = Recommender.objects.create()
4164        cls.t2 = Recommendation.objects.create(the_recommender=cls.t1)
4165        cls.t3 = Recommender.objects.create()
4166        cls.t4 = Recommendation.objects.create(the_recommender=cls.t3)
4167
4168        cls.tt1 = TitleTranslation.objects.create(title=cls.t1, text="Bar")
4169        cls.tt2 = TitleTranslation.objects.create(title=cls.t2, text="Foo")
4170        cls.tt3 = TitleTranslation.objects.create(title=cls.t3, text="Few")
4171        cls.tt4 = TitleTranslation.objects.create(title=cls.t4, text="Bas")
4172
4173    def setUp(self):
4174        self.client.force_login(self.superuser)
4175
4176    def test_search_on_sibling_models(self):
4177        "A search that mentions sibling models"
4178        response = self.client.get(
4179            reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4180        )
4181        # confirm the search returned 1 object
4182        self.assertContains(response, "\n1 recommendation\n")
4183
4184    def test_with_fk_to_field(self):
4185        """
4186        The to_field GET parameter is preserved when a search is performed.
4187        Refs #10918.
4188        """
4189        response = self.client.get(
4190            reverse("admin:auth_user_changelist") + "?q=joe&%s=id" % TO_FIELD_VAR
4191        )
4192        self.assertContains(response, "\n1 user\n")
4193        self.assertContains(
4194            response,
4195            '<input type="hidden" name="%s" value="id">' % TO_FIELD_VAR,
4196            html=True,
4197        )
4198
4199    def test_exact_matches(self):
4200        response = self.client.get(
4201            reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4202        )
4203        # confirm the search returned one object
4204        self.assertContains(response, "\n1 recommendation\n")
4205
4206        response = self.client.get(
4207            reverse("admin:admin_views_recommendation_changelist") + "?q=ba"
4208        )
4209        # confirm the search returned zero objects
4210        self.assertContains(response, "\n0 recommendations\n")
4211
4212    def test_beginning_matches(self):
4213        response = self.client.get(
4214            reverse("admin:admin_views_person_changelist") + "?q=Gui"
4215        )
4216        # confirm the search returned one object
4217        self.assertContains(response, "\n1 person\n")
4218        self.assertContains(response, "Guido")
4219
4220        response = self.client.get(
4221            reverse("admin:admin_views_person_changelist") + "?q=uido"
4222        )
4223        # confirm the search returned zero objects
4224        self.assertContains(response, "\n0 persons\n")
4225        self.assertNotContains(response, "Guido")
4226
4227    def test_pluggable_search(self):
4228        PluggableSearchPerson.objects.create(name="Bob", age=10)
4229        PluggableSearchPerson.objects.create(name="Amy", age=20)
4230
4231        response = self.client.get(
4232            reverse("admin:admin_views_pluggablesearchperson_changelist") + "?q=Bob"
4233        )
4234        # confirm the search returned one object
4235        self.assertContains(response, "\n1 pluggable search person\n")
4236        self.assertContains(response, "Bob")
4237
4238        response = self.client.get(
4239            reverse("admin:admin_views_pluggablesearchperson_changelist") + "?q=20"
4240        )
4241        # confirm the search returned one object
4242        self.assertContains(response, "\n1 pluggable search person\n")
4243        self.assertContains(response, "Amy")
4244
4245    def test_reset_link(self):
4246        """
4247        Test presence of reset link in search bar ("1 result (_x total_)").
4248        """
4249        #   1 query for session + 1 for fetching user
4250        # + 1 for filtered result + 1 for filtered count
4251        # + 1 for total count
4252        with self.assertNumQueries(5):
4253            response = self.client.get(
4254                reverse("admin:admin_views_person_changelist") + "?q=Gui"
4255            )
4256        self.assertContains(
4257            response,
4258            """<span class="small quiet">1 result (<a href="?">3 total</a>)</span>""",
4259            html=True,
4260        )
4261
4262    def test_no_total_count(self):
4263        """
4264        #8408 -- "Show all" should be displayed instead of the total count if
4265        ModelAdmin.show_full_result_count is False.
4266        """
4267        #   1 query for session + 1 for fetching user
4268        # + 1 for filtered result + 1 for filtered count
4269        with self.assertNumQueries(4):
4270            response = self.client.get(
4271                reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4272            )
4273        self.assertContains(
4274            response,
4275            """<span class="small quiet">1 result (<a href="?">Show all</a>)</span>""",
4276            html=True,
4277        )
4278        self.assertTrue(response.context["cl"].show_admin_actions)
4279
4280
4281@override_settings(ROOT_URLCONF="admin_views.urls")
4282class AdminInheritedInlinesTest(TestCase):
4283    @classmethod
4284    def setUpTestData(cls):
4285        cls.superuser = User.objects.create_superuser(
4286            username="super", password="secret", email="super@example.com"
4287        )
4288
4289    def setUp(self):
4290        self.client.force_login(self.superuser)
4291
4292    def test_inline(self):
4293        """
4294        Inline models which inherit from a common parent are correctly handled.
4295        """
4296        foo_user = "foo username"
4297        bar_user = "bar username"
4298
4299        name_re = re.compile(b'name="(.*?)"')
4300
4301        # test the add case
4302        response = self.client.get(reverse("admin:admin_views_persona_add"))
4303        names = name_re.findall(response.content)
4304        # make sure we have no duplicate HTML names
4305        self.assertEqual(len(names), len(set(names)))
4306
4307        # test the add case
4308        post_data = {
4309            "name": "Test Name",
4310            # inline data
4311            "accounts-TOTAL_FORMS": "1",
4312            "accounts-INITIAL_FORMS": "0",
4313            "accounts-MAX_NUM_FORMS": "0",
4314            "accounts-0-username": foo_user,
4315            "accounts-2-TOTAL_FORMS": "1",
4316            "accounts-2-INITIAL_FORMS": "0",
4317            "accounts-2-MAX_NUM_FORMS": "0",
4318            "accounts-2-0-username": bar_user,
4319        }
4320
4321        response = self.client.post(reverse("admin:admin_views_persona_add"), post_data)
4322        self.assertEqual(response.status_code, 302)  # redirect somewhere
4323        self.assertEqual(Persona.objects.count(), 1)
4324        self.assertEqual(FooAccount.objects.count(), 1)
4325        self.assertEqual(BarAccount.objects.count(), 1)
4326        self.assertEqual(FooAccount.objects.all()[0].username, foo_user)
4327        self.assertEqual(BarAccount.objects.all()[0].username, bar_user)
4328        self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
4329
4330        persona_id = Persona.objects.all()[0].id
4331        foo_id = FooAccount.objects.all()[0].id
4332        bar_id = BarAccount.objects.all()[0].id
4333
4334        # test the edit case
4335
4336        response = self.client.get(
4337            reverse("admin:admin_views_persona_change", args=(persona_id,))
4338        )
4339        names = name_re.findall(response.content)
4340        # make sure we have no duplicate HTML names
4341        self.assertEqual(len(names), len(set(names)))
4342
4343        post_data = {
4344            "name": "Test Name",
4345            "accounts-TOTAL_FORMS": "2",
4346            "accounts-INITIAL_FORMS": "1",
4347            "accounts-MAX_NUM_FORMS": "0",
4348            "accounts-0-username": "%s-1" % foo_user,
4349            "accounts-0-account_ptr": str(foo_id),
4350            "accounts-0-persona": str(persona_id),
4351            "accounts-2-TOTAL_FORMS": "2",
4352            "accounts-2-INITIAL_FORMS": "1",
4353            "accounts-2-MAX_NUM_FORMS": "0",
4354            "accounts-2-0-username": "%s-1" % bar_user,
4355            "accounts-2-0-account_ptr": str(bar_id),
4356            "accounts-2-0-persona": str(persona_id),
4357        }
4358        response = self.client.post(
4359            reverse("admin:admin_views_persona_change", args=(persona_id,)), post_data
4360        )
4361        self.assertEqual(response.status_code, 302)
4362        self.assertEqual(Persona.objects.count(), 1)
4363        self.assertEqual(FooAccount.objects.count(), 1)
4364        self.assertEqual(BarAccount.objects.count(), 1)
4365        self.assertEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
4366        self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
4367        self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
4368
4369
4370@override_settings(ROOT_URLCONF="admin_views.urls")
4371class TestCustomChangeList(TestCase):
4372    @classmethod
4373    def setUpTestData(cls):
4374        cls.superuser = User.objects.create_superuser(
4375            username="super", password="secret", email="super@example.com"
4376        )
4377
4378    def setUp(self):
4379        self.client.force_login(self.superuser)
4380
4381    def test_custom_changelist(self):
4382        """
4383        Validate that a custom ChangeList class can be used (#9749)
4384        """
4385        # Insert some data
4386        post_data = {"name": "First Gadget"}
4387        response = self.client.post(reverse("admin:admin_views_gadget_add"), post_data)
4388        self.assertEqual(response.status_code, 302)  # redirect somewhere
4389        # Hit the page once to get messages out of the queue message list
4390        response = self.client.get(reverse("admin:admin_views_gadget_changelist"))
4391        # Data is still not visible on the page
4392        response = self.client.get(reverse("admin:admin_views_gadget_changelist"))
4393        self.assertEqual(response.status_code, 200)
4394        self.assertNotContains(response, "First Gadget")
4395
4396
4397@override_settings(ROOT_URLCONF="admin_views.urls")
4398class TestInlineNotEditable(TestCase):
4399    @classmethod
4400    def setUpTestData(cls):
4401        cls.superuser = User.objects.create_superuser(
4402            username="super", password="secret", email="super@example.com"
4403        )
4404
4405    def setUp(self):
4406        self.client.force_login(self.superuser)
4407
4408    def test_GET_parent_add(self):
4409        """
4410        InlineModelAdmin broken?
4411        """
4412        response = self.client.get(reverse("admin:admin_views_parent_add"))
4413        self.assertEqual(response.status_code, 200)
4414
4415
4416@override_settings(ROOT_URLCONF="admin_views.urls")
4417class AdminCustomQuerysetTest(TestCase):
4418    @classmethod
4419    def setUpTestData(cls):
4420        cls.superuser = User.objects.create_superuser(
4421            username="super", password="secret", email="super@example.com"
4422        )
4423        cls.pks = [EmptyModel.objects.create().id for i in range(3)]
4424
4425    def setUp(self):
4426        self.client.force_login(self.superuser)
4427        self.super_login = {
4428            REDIRECT_FIELD_NAME: reverse("admin:index"),
4429            "username": "super",
4430            "password": "secret",
4431        }
4432
4433    def test_changelist_view(self):
4434        response = self.client.get(reverse("admin:admin_views_emptymodel_changelist"))
4435        for i in self.pks:
4436            if i > 1:
4437                self.assertContains(response, "Primary key = %s" % i)
4438            else:
4439                self.assertNotContains(response, "Primary key = %s" % i)
4440
4441    def test_changelist_view_count_queries(self):
4442        # create 2 Person objects
4443        Person.objects.create(name="person1", gender=1)
4444        Person.objects.create(name="person2", gender=2)
4445        changelist_url = reverse("admin:admin_views_person_changelist")
4446
4447        # 5 queries are expected: 1 for the session, 1 for the user,
4448        # 2 for the counts and 1 for the objects on the page
4449        with self.assertNumQueries(5):
4450            resp = self.client.get(changelist_url)
4451            self.assertEqual(resp.context["selection_note"], "0 of 2 selected")
4452            self.assertEqual(resp.context["selection_note_all"], "All 2 selected")
4453        with self.assertNumQueries(5):
4454            extra = {"q": "not_in_name"}
4455            resp = self.client.get(changelist_url, extra)
4456            self.assertEqual(resp.context["selection_note"], "0 of 0 selected")
4457            self.assertEqual(resp.context["selection_note_all"], "All 0 selected")
4458        with self.assertNumQueries(5):
4459            extra = {"q": "person"}
4460            resp = self.client.get(changelist_url, extra)
4461            self.assertEqual(resp.context["selection_note"], "0 of 2 selected")
4462            self.assertEqual(resp.context["selection_note_all"], "All 2 selected")
4463        with self.assertNumQueries(5):
4464            extra = {"gender__exact": "1"}
4465            resp = self.client.get(changelist_url, extra)
4466            self.assertEqual(resp.context["selection_note"], "0 of 1 selected")
4467            self.assertEqual(resp.context["selection_note_all"], "1 selected")
4468
4469    def test_change_view(self):
4470        for i in self.pks:
4471            url = reverse("admin:admin_views_emptymodel_change", args=(i,))
4472            response = self.client.get(url, follow=True)
4473            if i > 1:
4474                self.assertEqual(response.status_code, 200)
4475            else:
4476                self.assertRedirects(response, reverse("admin:index"))
4477                self.assertEqual(
4478                    [m.message for m in response.context["messages"]],
4479                    ["empty model with ID “1” doesn’t exist. Perhaps it was deleted?"],
4480                )
4481
4482    def test_add_model_modeladmin_defer_qs(self):
4483        # Test for #14529. defer() is used in ModelAdmin.get_queryset()
4484
4485        # model has __str__ method
4486        self.assertEqual(CoverLetter.objects.count(), 0)
4487        # Emulate model instance creation via the admin
4488        post_data = {
4489            "author": "Candidate, Best",
4490            "_save": "Save",
4491        }
4492        response = self.client.post(
4493            reverse("admin:admin_views_coverletter_add"), post_data, follow=True
4494        )
4495        self.assertEqual(response.status_code, 200)
4496        self.assertEqual(CoverLetter.objects.count(), 1)
4497        # Message should contain non-ugly model verbose name
4498        pk = CoverLetter.objects.all()[0].pk
4499        self.assertContains(
4500            response,
4501            '<li class="success">The cover letter “<a href="%s">'
4502            "Candidate, Best</a>” was added successfully.</li>"
4503            % reverse("admin:admin_views_coverletter_change", args=(pk,)),
4504            html=True,
4505        )
4506
4507        # model has no __str__ method
4508        self.assertEqual(ShortMessage.objects.count(), 0)
4509        # Emulate model instance creation via the admin
4510        post_data = {
4511            "content": "What's this SMS thing?",
4512            "_save": "Save",
4513        }
4514        response = self.client.post(
4515            reverse("admin:admin_views_shortmessage_add"), post_data, follow=True
4516        )
4517        self.assertEqual(response.status_code, 200)
4518        self.assertEqual(ShortMessage.objects.count(), 1)
4519        # Message should contain non-ugly model verbose name
4520        sm = ShortMessage.objects.all()[0]
4521        self.assertContains(
4522            response,
4523            '<li class="success">The short message “<a href="%s">'
4524            "%s</a>” was added successfully.</li>"
4525            % (reverse("admin:admin_views_shortmessage_change", args=(sm.pk,)), sm),
4526            html=True,
4527        )
4528
4529    def test_add_model_modeladmin_only_qs(self):
4530        # Test for #14529. only() is used in ModelAdmin.get_queryset()
4531
4532        # model has __str__ method
4533        self.assertEqual(Telegram.objects.count(), 0)
4534        # Emulate model instance creation via the admin
4535        post_data = {
4536            "title": "Urgent telegram",
4537            "_save": "Save",
4538        }
4539        response = self.client.post(
4540            reverse("admin:admin_views_telegram_add"), post_data, follow=True
4541        )
4542        self.assertEqual(response.status_code, 200)
4543        self.assertEqual(Telegram.objects.count(), 1)
4544        # Message should contain non-ugly model verbose name
4545        pk = Telegram.objects.all()[0].pk
4546        self.assertContains(
4547            response,
4548            '<li class="success">The telegram “<a href="%s">'
4549            "Urgent telegram</a>” was added successfully.</li>"
4550            % reverse("admin:admin_views_telegram_change", args=(pk,)),
4551            html=True,
4552        )
4553
4554        # model has no __str__ method
4555        self.assertEqual(Paper.objects.count(), 0)
4556        # Emulate model instance creation via the admin
4557        post_data = {
4558            "title": "My Modified Paper Title",
4559            "_save": "Save",
4560        }
4561        response = self.client.post(
4562            reverse("admin:admin_views_paper_add"), post_data, follow=True
4563        )
4564        self.assertEqual(response.status_code, 200)
4565        self.assertEqual(Paper.objects.count(), 1)
4566        # Message should contain non-ugly model verbose name
4567        p = Paper.objects.all()[0]
4568        self.assertContains(
4569            response,
4570            '<li class="success">The paper “<a href="%s">'
4571            "%s</a>” was added successfully.</li>"
4572            % (reverse("admin:admin_views_paper_change", args=(p.pk,)), p),
4573            html=True,
4574        )
4575
4576    def test_edit_model_modeladmin_defer_qs(self):
4577        # Test for #14529. defer() is used in ModelAdmin.get_queryset()
4578
4579        # model has __str__ method
4580        cl = CoverLetter.objects.create(author="John Doe")
4581        self.assertEqual(CoverLetter.objects.count(), 1)
4582        response = self.client.get(
4583            reverse("admin:admin_views_coverletter_change", args=(cl.pk,))
4584        )
4585        self.assertEqual(response.status_code, 200)
4586        # Emulate model instance edit via the admin
4587        post_data = {
4588            "author": "John Doe II",
4589            "_save": "Save",
4590        }
4591        url = reverse("admin:admin_views_coverletter_change", args=(cl.pk,))
4592        response = self.client.post(url, post_data, follow=True)
4593        self.assertEqual(response.status_code, 200)
4594        self.assertEqual(CoverLetter.objects.count(), 1)
4595        # Message should contain non-ugly model verbose name. Instance
4596        # representation is set by model's __str__()
4597        self.assertContains(
4598            response,
4599            '<li class="success">The cover letter “<a href="%s">'
4600            "John Doe II</a>” was changed successfully.</li>"
4601            % reverse("admin:admin_views_coverletter_change", args=(cl.pk,)),
4602            html=True,
4603        )
4604
4605        # model has no __str__ method
4606        sm = ShortMessage.objects.create(content="This is expensive")
4607        self.assertEqual(ShortMessage.objects.count(), 1)
4608        response = self.client.get(
4609            reverse("admin:admin_views_shortmessage_change", args=(sm.pk,))
4610        )
4611        self.assertEqual(response.status_code, 200)
4612        # Emulate model instance edit via the admin
4613        post_data = {
4614            "content": "Too expensive",
4615            "_save": "Save",
4616        }
4617        url = reverse("admin:admin_views_shortmessage_change", args=(sm.pk,))
4618        response = self.client.post(url, post_data, follow=True)
4619        self.assertEqual(response.status_code, 200)
4620        self.assertEqual(ShortMessage.objects.count(), 1)
4621        # Message should contain non-ugly model verbose name. The ugly(!)
4622        # instance representation is set by __str__().
4623        self.assertContains(
4624            response,
4625            '<li class="success">The short message “<a href="%s">'
4626            "%s</a>” was changed successfully.</li>"
4627            % (reverse("admin:admin_views_shortmessage_change", args=(sm.pk,)), sm),
4628            html=True,
4629        )
4630
4631    def test_edit_model_modeladmin_only_qs(self):
4632        # Test for #14529. only() is used in ModelAdmin.get_queryset()
4633
4634        # model has __str__ method
4635        t = Telegram.objects.create(title="First Telegram")
4636        self.assertEqual(Telegram.objects.count(), 1)
4637        response = self.client.get(
4638            reverse("admin:admin_views_telegram_change", args=(t.pk,))
4639        )
4640        self.assertEqual(response.status_code, 200)
4641        # Emulate model instance edit via the admin
4642        post_data = {
4643            "title": "Telegram without typo",
4644            "_save": "Save",
4645        }
4646        response = self.client.post(
4647            reverse("admin:admin_views_telegram_change", args=(t.pk,)),
4648            post_data,
4649            follow=True,
4650        )
4651        self.assertEqual(response.status_code, 200)
4652        self.assertEqual(Telegram.objects.count(), 1)
4653        # Message should contain non-ugly model verbose name. The instance
4654        # representation is set by model's __str__()
4655        self.assertContains(
4656            response,
4657            '<li class="success">The telegram “<a href="%s">'
4658            "Telegram without typo</a>” was changed successfully.</li>"
4659            % reverse("admin:admin_views_telegram_change", args=(t.pk,)),
4660            html=True,
4661        )
4662
4663        # model has no __str__ method
4664        p = Paper.objects.create(title="My Paper Title")
4665        self.assertEqual(Paper.objects.count(), 1)
4666        response = self.client.get(
4667            reverse("admin:admin_views_paper_change", args=(p.pk,))
4668        )
4669        self.assertEqual(response.status_code, 200)
4670        # Emulate model instance edit via the admin
4671        post_data = {
4672            "title": "My Modified Paper Title",
4673            "_save": "Save",
4674        }
4675        response = self.client.post(
4676            reverse("admin:admin_views_paper_change", args=(p.pk,)),
4677            post_data,
4678            follow=True,
4679        )
4680        self.assertEqual(response.status_code, 200)
4681        self.assertEqual(Paper.objects.count(), 1)
4682        # Message should contain non-ugly model verbose name. The ugly(!)
4683        # instance representation is set by __str__().
4684        self.assertContains(
4685            response,
4686            '<li class="success">The paper “<a href="%s">'
4687            "%s</a>” was changed successfully.</li>"
4688            % (reverse("admin:admin_views_paper_change", args=(p.pk,)), p),
4689            html=True,
4690        )
4691
4692    def test_history_view_custom_qs(self):
4693        """
4694        Custom querysets are considered for the admin history view.
4695        """
4696        self.client.post(reverse("admin:login"), self.super_login)
4697        FilteredManager.objects.create(pk=1)
4698        FilteredManager.objects.create(pk=2)
4699        response = self.client.get(
4700            reverse("admin:admin_views_filteredmanager_changelist")
4701        )
4702        self.assertContains(response, "PK=1")
4703        self.assertContains(response, "PK=2")
4704        self.assertEqual(
4705            self.client.get(
4706                reverse("admin:admin_views_filteredmanager_history", args=(1,))
4707            ).status_code,
4708            200,
4709        )
4710        self.assertEqual(
4711            self.client.get(
4712                reverse("admin:admin_views_filteredmanager_history", args=(2,))
4713            ).status_code,
4714            200,
4715        )
4716
4717
4718@override_settings(ROOT_URLCONF="admin_views.urls")
4719class AdminInlineFileUploadTest(TestCase):
4720    @classmethod
4721    def setUpTestData(cls):
4722        cls.superuser = User.objects.create_superuser(
4723            username="super", password="secret", email="super@example.com"
4724        )
4725        file1 = tempfile.NamedTemporaryFile(suffix=".file1")
4726        file1.write(b"a" * (2 ** 21))
4727        filename = file1.name
4728        file1.close()
4729        cls.gallery = Gallery.objects.create(name="Test Gallery")
4730        cls.picture = Picture.objects.create(
4731            name="Test Picture", image=filename, gallery=cls.gallery,
4732        )
4733
4734    def setUp(self):
4735        self.client.force_login(self.superuser)
4736
4737    def test_form_has_multipart_enctype(self):
4738        response = self.client.get(
4739            reverse("admin:admin_views_gallery_change", args=(self.gallery.id,))
4740        )
4741        self.assertIs(response.context["has_file_field"], True)
4742        self.assertContains(response, MULTIPART_ENCTYPE)
4743
4744    def test_inline_file_upload_edit_validation_error_post(self):
4745        """
4746        Inline file uploads correctly display prior data (#10002).
4747        """
4748        post_data = {
4749            "name": "Test Gallery",
4750            "pictures-TOTAL_FORMS": "2",
4751            "pictures-INITIAL_FORMS": "1",
4752            "pictures-MAX_NUM_FORMS": "0",
4753            "pictures-0-id": str(self.picture.id),
4754            "pictures-0-gallery": str(self.gallery.id),
4755            "pictures-0-name": "Test Picture",
4756            "pictures-0-image": "",
4757            "pictures-1-id": "",
4758            "pictures-1-gallery": str(self.gallery.id),
4759            "pictures-1-name": "Test Picture 2",
4760            "pictures-1-image": "",
4761        }
4762        response = self.client.post(
4763            reverse("admin:admin_views_gallery_change", args=(self.gallery.id,)),
4764            post_data,
4765        )
4766        self.assertContains(response, b"Currently")
4767
4768
4769@override_settings(ROOT_URLCONF="admin_views.urls")
4770class AdminInlineTests(TestCase):
4771    @classmethod
4772    def setUpTestData(cls):
4773        cls.superuser = User.objects.create_superuser(
4774            username="super", password="secret", email="super@example.com"
4775        )
4776        cls.collector = Collector.objects.create(pk=1, name="John Fowles")
4777
4778    def setUp(self):
4779        self.post_data = {
4780            "name": "Test Name",
4781            "widget_set-TOTAL_FORMS": "3",
4782            "widget_set-INITIAL_FORMS": "0",
4783            "widget_set-MAX_NUM_FORMS": "0",
4784            "widget_set-0-id": "",
4785            "widget_set-0-owner": "1",
4786            "widget_set-0-name": "",
4787            "widget_set-1-id": "",
4788            "widget_set-1-owner": "1",
4789            "widget_set-1-name": "",
4790            "widget_set-2-id": "",
4791            "widget_set-2-owner": "1",
4792            "widget_set-2-name": "",
4793            "doohickey_set-TOTAL_FORMS": "3",
4794            "doohickey_set-INITIAL_FORMS": "0",
4795            "doohickey_set-MAX_NUM_FORMS": "0",
4796            "doohickey_set-0-owner": "1",
4797            "doohickey_set-0-code": "",
4798            "doohickey_set-0-name": "",
4799            "doohickey_set-1-owner": "1",
4800            "doohickey_set-1-code": "",
4801            "doohickey_set-1-name": "",
4802            "doohickey_set-2-owner": "1",
4803            "doohickey_set-2-code": "",
4804            "doohickey_set-2-name": "",
4805            "grommet_set-TOTAL_FORMS": "3",
4806            "grommet_set-INITIAL_FORMS": "0",
4807            "grommet_set-MAX_NUM_FORMS": "0",
4808            "grommet_set-0-code": "",
4809            "grommet_set-0-owner": "1",
4810            "grommet_set-0-name": "",
4811            "grommet_set-1-code": "",
4812            "grommet_set-1-owner": "1",
4813            "grommet_set-1-name": "",
4814            "grommet_set-2-code": "",
4815            "grommet_set-2-owner": "1",
4816            "grommet_set-2-name": "",
4817            "whatsit_set-TOTAL_FORMS": "3",
4818            "whatsit_set-INITIAL_FORMS": "0",
4819            "whatsit_set-MAX_NUM_FORMS": "0",
4820            "whatsit_set-0-owner": "1",
4821            "whatsit_set-0-index": "",
4822            "whatsit_set-0-name": "",
4823            "whatsit_set-1-owner": "1",
4824            "whatsit_set-1-index": "",
4825            "whatsit_set-1-name": "",
4826            "whatsit_set-2-owner": "1",
4827            "whatsit_set-2-index": "",
4828            "whatsit_set-2-name": "",
4829            "fancydoodad_set-TOTAL_FORMS": "3",
4830            "fancydoodad_set-INITIAL_FORMS": "0",
4831            "fancydoodad_set-MAX_NUM_FORMS": "0",
4832            "fancydoodad_set-0-doodad_ptr": "",
4833            "fancydoodad_set-0-owner": "1",
4834            "fancydoodad_set-0-name": "",
4835            "fancydoodad_set-0-expensive": "on",
4836            "fancydoodad_set-1-doodad_ptr": "",
4837            "fancydoodad_set-1-owner": "1",
4838            "fancydoodad_set-1-name": "",
4839            "fancydoodad_set-1-expensive": "on",
4840            "fancydoodad_set-2-doodad_ptr": "",
4841            "fancydoodad_set-2-owner": "1",
4842            "fancydoodad_set-2-name": "",
4843            "fancydoodad_set-2-expensive": "on",
4844            "category_set-TOTAL_FORMS": "3",
4845            "category_set-INITIAL_FORMS": "0",
4846            "category_set-MAX_NUM_FORMS": "0",
4847            "category_set-0-order": "",
4848            "category_set-0-id": "",
4849            "category_set-0-collector": "1",
4850            "category_set-1-order": "",
4851            "category_set-1-id": "",
4852            "category_set-1-collector": "1",
4853            "category_set-2-order": "",
4854            "category_set-2-id": "",
4855            "category_set-2-collector": "1",
4856        }
4857
4858        self.client.force_login(self.superuser)
4859
4860    def test_simple_inline(self):
4861        "A simple model can be saved as inlines"
4862        # First add a new inline
4863        self.post_data["widget_set-0-name"] = "Widget 1"
4864        collector_url = reverse(
4865            "admin:admin_views_collector_change", args=(self.collector.pk,)
4866        )
4867        response = self.client.post(collector_url, self.post_data)
4868        self.assertEqual(response.status_code, 302)
4869        self.assertEqual(Widget.objects.count(), 1)
4870        self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
4871        widget_id = Widget.objects.all()[0].id
4872
4873        # The PK link exists on the rendered form
4874        response = self.client.get(collector_url)
4875        self.assertContains(response, 'name="widget_set-0-id"')
4876
4877        # No file or image fields, no enctype on the forms
4878        self.assertIs(response.context["has_file_field"], False)
4879        self.assertNotContains(response, MULTIPART_ENCTYPE)
4880
4881        # Now resave that inline
4882        self.post_data["widget_set-INITIAL_FORMS"] = "1"
4883        self.post_data["widget_set-0-id"] = str(widget_id)
4884        self.post_data["widget_set-0-name"] = "Widget 1"
4885        response = self.client.post(collector_url, self.post_data)
4886        self.assertEqual(response.status_code, 302)
4887        self.assertEqual(Widget.objects.count(), 1)
4888        self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
4889
4890        # Now modify that inline
4891        self.post_data["widget_set-INITIAL_FORMS"] = "1"
4892        self.post_data["widget_set-0-id"] = str(widget_id)
4893        self.post_data["widget_set-0-name"] = "Widget 1 Updated"
4894        response = self.client.post(collector_url, self.post_data)
4895        self.assertEqual(response.status_code, 302)
4896        self.assertEqual(Widget.objects.count(), 1)
4897        self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated")
4898
4899    def test_explicit_autofield_inline(self):
4900        "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093"
4901        # First add a new inline
4902        self.post_data["grommet_set-0-name"] = "Grommet 1"
4903        collector_url = reverse(
4904            "admin:admin_views_collector_change", args=(self.collector.pk,)
4905        )
4906        response = self.client.post(collector_url, self.post_data)
4907        self.assertEqual(response.status_code, 302)
4908        self.assertEqual(Grommet.objects.count(), 1)
4909        self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
4910
4911        # The PK link exists on the rendered form
4912        response = self.client.get(collector_url)
4913        self.assertContains(response, 'name="grommet_set-0-code"')
4914
4915        # Now resave that inline
4916        self.post_data["grommet_set-INITIAL_FORMS"] = "1"
4917        self.post_data["grommet_set-0-code"] = str(Grommet.objects.all()[0].code)
4918        self.post_data["grommet_set-0-name"] = "Grommet 1"
4919        response = self.client.post(collector_url, self.post_data)
4920        self.assertEqual(response.status_code, 302)
4921        self.assertEqual(Grommet.objects.count(), 1)
4922        self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
4923
4924        # Now modify that inline
4925        self.post_data["grommet_set-INITIAL_FORMS"] = "1"
4926        self.post_data["grommet_set-0-code"] = str(Grommet.objects.all()[0].code)
4927        self.post_data["grommet_set-0-name"] = "Grommet 1 Updated"
4928        response = self.client.post(collector_url, self.post_data)
4929        self.assertEqual(response.status_code, 302)
4930        self.assertEqual(Grommet.objects.count(), 1)
4931        self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated")
4932
4933    def test_char_pk_inline(self):
4934        "A model with a character PK can be saved as inlines. Regression for #10992"
4935        # First add a new inline
4936        self.post_data["doohickey_set-0-code"] = "DH1"
4937        self.post_data["doohickey_set-0-name"] = "Doohickey 1"
4938        collector_url = reverse(
4939            "admin:admin_views_collector_change", args=(self.collector.pk,)
4940        )
4941        response = self.client.post(collector_url, self.post_data)
4942        self.assertEqual(response.status_code, 302)
4943        self.assertEqual(DooHickey.objects.count(), 1)
4944        self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
4945
4946        # The PK link exists on the rendered form
4947        response = self.client.get(collector_url)
4948        self.assertContains(response, 'name="doohickey_set-0-code"')
4949
4950        # Now resave that inline
4951        self.post_data["doohickey_set-INITIAL_FORMS"] = "1"
4952        self.post_data["doohickey_set-0-code"] = "DH1"
4953        self.post_data["doohickey_set-0-name"] = "Doohickey 1"
4954        response = self.client.post(collector_url, self.post_data)
4955        self.assertEqual(response.status_code, 302)
4956        self.assertEqual(DooHickey.objects.count(), 1)
4957        self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
4958
4959        # Now modify that inline
4960        self.post_data["doohickey_set-INITIAL_FORMS"] = "1"
4961        self.post_data["doohickey_set-0-code"] = "DH1"
4962        self.post_data["doohickey_set-0-name"] = "Doohickey 1 Updated"
4963        response = self.client.post(collector_url, self.post_data)
4964        self.assertEqual(response.status_code, 302)
4965        self.assertEqual(DooHickey.objects.count(), 1)
4966        self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated")
4967
4968    def test_integer_pk_inline(self):
4969        "A model with an integer PK can be saved as inlines. Regression for #10992"
4970        # First add a new inline
4971        self.post_data["whatsit_set-0-index"] = "42"
4972        self.post_data["whatsit_set-0-name"] = "Whatsit 1"
4973        collector_url = reverse(
4974            "admin:admin_views_collector_change", args=(self.collector.pk,)
4975        )
4976        response = self.client.post(collector_url, self.post_data)
4977        self.assertEqual(response.status_code, 302)
4978        self.assertEqual(Whatsit.objects.count(), 1)
4979        self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
4980
4981        # The PK link exists on the rendered form
4982        response = self.client.get(collector_url)
4983        self.assertContains(response, 'name="whatsit_set-0-index"')
4984
4985        # Now resave that inline
4986        self.post_data["whatsit_set-INITIAL_FORMS"] = "1"
4987        self.post_data["whatsit_set-0-index"] = "42"
4988        self.post_data["whatsit_set-0-name"] = "Whatsit 1"
4989        response = self.client.post(collector_url, self.post_data)
4990        self.assertEqual(response.status_code, 302)
4991        self.assertEqual(Whatsit.objects.count(), 1)
4992        self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
4993
4994        # Now modify that inline
4995        self.post_data["whatsit_set-INITIAL_FORMS"] = "1"
4996        self.post_data["whatsit_set-0-index"] = "42"
4997        self.post_data["whatsit_set-0-name"] = "Whatsit 1 Updated"
4998        response = self.client.post(collector_url, self.post_data)
4999        self.assertEqual(response.status_code, 302)
5000        self.assertEqual(Whatsit.objects.count(), 1)
5001        self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated")
5002
5003    def test_inherited_inline(self):
5004        "An inherited model can be saved as inlines. Regression for #11042"
5005        # First add a new inline
5006        self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1"
5007        collector_url = reverse(
5008            "admin:admin_views_collector_change", args=(self.collector.pk,)
5009        )
5010        response = self.client.post(collector_url, self.post_data)
5011        self.assertEqual(response.status_code, 302)
5012        self.assertEqual(FancyDoodad.objects.count(), 1)
5013        self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
5014        doodad_pk = FancyDoodad.objects.all()[0].pk
5015
5016        # The PK link exists on the rendered form
5017        response = self.client.get(collector_url)
5018        self.assertContains(response, 'name="fancydoodad_set-0-doodad_ptr"')
5019
5020        # Now resave that inline
5021        self.post_data["fancydoodad_set-INITIAL_FORMS"] = "1"
5022        self.post_data["fancydoodad_set-0-doodad_ptr"] = str(doodad_pk)
5023        self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1"
5024        response = self.client.post(collector_url, self.post_data)
5025        self.assertEqual(response.status_code, 302)
5026        self.assertEqual(FancyDoodad.objects.count(), 1)
5027        self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
5028
5029        # Now modify that inline
5030        self.post_data["fancydoodad_set-INITIAL_FORMS"] = "1"
5031        self.post_data["fancydoodad_set-0-doodad_ptr"] = str(doodad_pk)
5032        self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1 Updated"
5033        response = self.client.post(collector_url, self.post_data)
5034        self.assertEqual(response.status_code, 302)
5035        self.assertEqual(FancyDoodad.objects.count(), 1)
5036        self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated")
5037
5038    def test_ordered_inline(self):
5039        """
5040        An inline with an editable ordering fields is updated correctly.
5041        """
5042        # Create some objects with an initial ordering
5043        Category.objects.create(id=1, order=1, collector=self.collector)
5044        Category.objects.create(id=2, order=2, collector=self.collector)
5045        Category.objects.create(id=3, order=0, collector=self.collector)
5046        Category.objects.create(id=4, order=0, collector=self.collector)
5047
5048        # NB: The order values must be changed so that the items are reordered.
5049        self.post_data.update(
5050            {
5051                "name": "Frederick Clegg",
5052                "category_set-TOTAL_FORMS": "7",
5053                "category_set-INITIAL_FORMS": "4",
5054                "category_set-MAX_NUM_FORMS": "0",
5055                "category_set-0-order": "14",
5056                "category_set-0-id": "1",
5057                "category_set-0-collector": "1",
5058                "category_set-1-order": "13",
5059                "category_set-1-id": "2",
5060                "category_set-1-collector": "1",
5061                "category_set-2-order": "1",
5062                "category_set-2-id": "3",
5063                "category_set-2-collector": "1",
5064                "category_set-3-order": "0",
5065                "category_set-3-id": "4",
5066                "category_set-3-collector": "1",
5067                "category_set-4-order": "",
5068                "category_set-4-id": "",
5069                "category_set-4-collector": "1",
5070                "category_set-5-order": "",
5071                "category_set-5-id": "",
5072                "category_set-5-collector": "1",
5073                "category_set-6-order": "",
5074                "category_set-6-id": "",
5075                "category_set-6-collector": "1",
5076            }
5077        )
5078        collector_url = reverse(
5079            "admin:admin_views_collector_change", args=(self.collector.pk,)
5080        )
5081        response = self.client.post(collector_url, self.post_data)
5082        # Successful post will redirect
5083        self.assertEqual(response.status_code, 302)
5084
5085        # The order values have been applied to the right objects
5086        self.assertEqual(self.collector.category_set.count(), 4)
5087        self.assertEqual(Category.objects.get(id=1).order, 14)
5088        self.assertEqual(Category.objects.get(id=2).order, 13)
5089        self.assertEqual(Category.objects.get(id=3).order, 1)
5090        self.assertEqual(Category.objects.get(id=4).order, 0)
5091
5092
5093@override_settings(ROOT_URLCONF="admin_views.urls")
5094class NeverCacheTests(TestCase):
5095    @classmethod
5096    def setUpTestData(cls):
5097        cls.superuser = User.objects.create_superuser(
5098            username="super", password="secret", email="super@example.com"
5099        )
5100        cls.s1 = Section.objects.create(name="Test section")
5101
5102    def setUp(self):
5103        self.client.force_login(self.superuser)
5104
5105    def test_admin_index(self):
5106        "Check the never-cache status of the main index"
5107        response = self.client.get(reverse("admin:index"))
5108        self.assertEqual(get_max_age(response), 0)
5109
5110    def test_app_index(self):
5111        "Check the never-cache status of an application index"
5112        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
5113        self.assertEqual(get_max_age(response), 0)
5114
5115    def test_model_index(self):
5116        "Check the never-cache status of a model index"
5117        response = self.client.get(reverse("admin:admin_views_fabric_changelist"))
5118        self.assertEqual(get_max_age(response), 0)
5119
5120    def test_model_add(self):
5121        "Check the never-cache status of a model add page"
5122        response = self.client.get(reverse("admin:admin_views_fabric_add"))
5123        self.assertEqual(get_max_age(response), 0)
5124
5125    def test_model_view(self):
5126        "Check the never-cache status of a model edit page"
5127        response = self.client.get(
5128            reverse("admin:admin_views_section_change", args=(self.s1.pk,))
5129        )
5130        self.assertEqual(get_max_age(response), 0)
5131
5132    def test_model_history(self):
5133        "Check the never-cache status of a model history page"
5134        response = self.client.get(
5135            reverse("admin:admin_views_section_history", args=(self.s1.pk,))
5136        )
5137        self.assertEqual(get_max_age(response), 0)
5138
5139    def test_model_delete(self):
5140        "Check the never-cache status of a model delete page"
5141        response = self.client.get(
5142            reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
5143        )
5144        self.assertEqual(get_max_age(response), 0)
5145
5146    def test_login(self):
5147        "Check the never-cache status of login views"
5148        self.client.logout()
5149        response = self.client.get(reverse("admin:index"))
5150        self.assertEqual(get_max_age(response), 0)
5151
5152    def test_logout(self):
5153        "Check the never-cache status of logout view"
5154        response = self.client.get(reverse("admin:logout"))
5155        self.assertEqual(get_max_age(response), 0)
5156
5157    def test_password_change(self):
5158        "Check the never-cache status of the password change view"
5159        self.client.logout()
5160        response = self.client.get(reverse("admin:password_change"))
5161        self.assertIsNone(get_max_age(response))
5162
5163    def test_password_change_done(self):
5164        "Check the never-cache status of the password change done view"
5165        response = self.client.get(reverse("admin:password_change_done"))
5166        self.assertIsNone(get_max_age(response))
5167
5168    def test_JS_i18n(self):
5169        "Check the never-cache status of the JavaScript i18n view"
5170        response = self.client.get(reverse("admin:jsi18n"))
5171        self.assertIsNone(get_max_age(response))
5172
5173
5174@override_settings(ROOT_URLCONF="admin_views.urls")
5175class PrePopulatedTest(TestCase):
5176    @classmethod
5177    def setUpTestData(cls):
5178        cls.superuser = User.objects.create_superuser(
5179            username="super", password="secret", email="super@example.com"
5180        )
5181        cls.p1 = PrePopulatedPost.objects.create(
5182            title="A Long Title", published=True, slug="a-long-title"
5183        )
5184
5185    def setUp(self):
5186        self.client.force_login(self.superuser)
5187
5188    def test_prepopulated_on(self):
5189        response = self.client.get(reverse("admin:admin_views_prepopulatedpost_add"))
5190        self.assertContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
5191        self.assertContains(
5192            response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]"
5193        )
5194        self.assertContains(
5195            response,
5196            "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;",
5197        )
5198
5199    def test_prepopulated_off(self):
5200        response = self.client.get(
5201            reverse("admin:admin_views_prepopulatedpost_change", args=(self.p1.pk,))
5202        )
5203        self.assertContains(response, "A Long Title")
5204        self.assertNotContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
5205        self.assertNotContains(
5206            response, "&quot;dependency_ids&quot;: [&quot;#id_title&quot;]"
5207        )
5208        self.assertNotContains(
5209            response,
5210            "&quot;id&quot;: &quot;#id_prepopulatedsubpost_set-0-subslug&quot;",
5211        )
5212
5213    @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
5214    def test_prepopulated_maxlength_localized(self):
5215        """
5216        Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure
5217        that maxLength (in the JavaScript) is rendered without separators.
5218        """
5219        response = self.client.get(
5220            reverse("admin:admin_views_prepopulatedpostlargeslug_add")
5221        )
5222        self.assertContains(response, "&quot;maxLength&quot;: 1000")  # instead of 1,000
5223
5224    def test_view_only_add_form(self):
5225        """
5226        PrePopulatedPostReadOnlyAdmin.prepopulated_fields includes 'slug'
5227        which is present in the add view, even if the
5228        ModelAdmin.has_change_permission() returns False.
5229        """
5230        response = self.client.get(reverse("admin7:admin_views_prepopulatedpost_add"))
5231        self.assertContains(response, "data-prepopulated-fields=")
5232        self.assertContains(response, "&quot;id&quot;: &quot;#id_slug&quot;")
5233
5234    def test_view_only_change_form(self):
5235        """
5236        PrePopulatedPostReadOnlyAdmin.prepopulated_fields includes 'slug'. That
5237        doesn't break a view-only change view.
5238        """
5239        response = self.client.get(
5240            reverse("admin7:admin_views_prepopulatedpost_change", args=(self.p1.pk,))
5241        )
5242        self.assertContains(response, 'data-prepopulated-fields="[]"')
5243        self.assertContains(response, '<div class="readonly">%s</div>' % self.p1.slug)
5244
5245
5246@override_settings(ROOT_URLCONF="admin_views.urls")
5247class SeleniumTests(AdminSeleniumTestCase):
5248
5249    available_apps = ["admin_views"] + AdminSeleniumTestCase.available_apps
5250
5251    def setUp(self):
5252        self.superuser = User.objects.create_superuser(
5253            username="super", password="secret", email="super@example.com"
5254        )
5255        self.p1 = PrePopulatedPost.objects.create(
5256            title="A Long Title", published=True, slug="a-long-title"
5257        )
5258
5259    def test_prepopulated_fields(self):
5260        """
5261        The JavaScript-automated prepopulated fields work with the main form
5262        and with stacked and tabular inlines.
5263        Refs #13068, #9264, #9983, #9784.
5264        """
5265        self.admin_login(
5266            username="super", password="secret", login_url=reverse("admin:index")
5267        )
5268        self.selenium.get(
5269            self.live_server_url + reverse("admin:admin_views_mainprepopulated_add")
5270        )
5271        self.wait_for(".select2")
5272
5273        # Main form ----------------------------------------------------------
5274        self.selenium.find_element_by_id("id_pubdate").send_keys("2012-02-18")
5275        self.select_option("#id_status", "option two")
5276        self.selenium.find_element_by_id("id_name").send_keys(
5277            " this is the mAin nÀMë and it's awεšomeııı"
5278        )
5279        slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5280        slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5281        slug3 = self.selenium.find_element_by_id("id_slug3").get_attribute("value")
5282        self.assertEqual(slug1, "main-name-and-its-awesomeiii-2012-02-18")
5283        self.assertEqual(slug2, "option-two-main-name-and-its-awesomeiii")
5284        self.assertEqual(
5285            slug3,
5286            "this-is-the-main-n\xe0m\xeb-and-its-aw\u03b5\u0161ome\u0131\u0131\u0131",
5287        )
5288
5289        # Stacked inlines ----------------------------------------------------
5290        # Initial inline
5291        self.selenium.find_element_by_id(
5292            "id_relatedprepopulated_set-0-pubdate"
5293        ).send_keys("2011-12-17")
5294        self.select_option("#id_relatedprepopulated_set-0-status", "option one")
5295        self.selenium.find_element_by_id("id_relatedprepopulated_set-0-name").send_keys(
5296            " here is a sŤāÇkeð   inline !  "
5297        )
5298        slug1 = self.selenium.find_element_by_id(
5299            "id_relatedprepopulated_set-0-slug1"
5300        ).get_attribute("value")
5301        slug2 = self.selenium.find_element_by_id(
5302            "id_relatedprepopulated_set-0-slug2"
5303        ).get_attribute("value")
5304        self.assertEqual(slug1, "here-stacked-inline-2011-12-17")
5305        self.assertEqual(slug2, "option-one-here-stacked-inline")
5306        initial_select2_inputs = self.selenium.find_elements_by_class_name(
5307            "select2-selection"
5308        )
5309        # Inline formsets have empty/invisible forms.
5310        # Only the 4 visible select2 inputs are initialized.
5311        num_initial_select2_inputs = len(initial_select2_inputs)
5312        self.assertEqual(num_initial_select2_inputs, 4)
5313
5314        # Add an inline
5315        self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5316            0
5317        ].click()
5318        self.assertEqual(
5319            len(self.selenium.find_elements_by_class_name("select2-selection")),
5320            num_initial_select2_inputs + 2,
5321        )
5322        self.selenium.find_element_by_id(
5323            "id_relatedprepopulated_set-1-pubdate"
5324        ).send_keys("1999-01-25")
5325        self.select_option("#id_relatedprepopulated_set-1-status", "option two")
5326        self.selenium.find_element_by_id("id_relatedprepopulated_set-1-name").send_keys(
5327            " now you haVe anöther   sŤāÇkeð  inline with a very ... "
5328            "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... "
5329        )
5330        slug1 = self.selenium.find_element_by_id(
5331            "id_relatedprepopulated_set-1-slug1"
5332        ).get_attribute("value")
5333        slug2 = self.selenium.find_element_by_id(
5334            "id_relatedprepopulated_set-1-slug2"
5335        ).get_attribute("value")
5336        # 50 characters maximum for slug1 field
5337        self.assertEqual(slug1, "now-you-have-another-stacked-inline-very-loooooooo")
5338        # 60 characters maximum for slug2 field
5339        self.assertEqual(
5340            slug2, "option-two-now-you-have-another-stacked-inline-very-looooooo"
5341        )
5342
5343        # Tabular inlines ----------------------------------------------------
5344        # Initial inline
5345        self.selenium.find_element_by_id(
5346            "id_relatedprepopulated_set-2-0-pubdate"
5347        ).send_keys("1234-12-07")
5348        self.select_option("#id_relatedprepopulated_set-2-0-status", "option two")
5349        self.selenium.find_element_by_id(
5350            "id_relatedprepopulated_set-2-0-name"
5351        ).send_keys("And now, with a tÃbűlaŘ inline !!!")
5352        slug1 = self.selenium.find_element_by_id(
5353            "id_relatedprepopulated_set-2-0-slug1"
5354        ).get_attribute("value")
5355        slug2 = self.selenium.find_element_by_id(
5356            "id_relatedprepopulated_set-2-0-slug2"
5357        ).get_attribute("value")
5358        self.assertEqual(slug1, "and-now-tabular-inline-1234-12-07")
5359        self.assertEqual(slug2, "option-two-and-now-tabular-inline")
5360
5361        # Add an inline
5362        self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5363            1
5364        ].click()
5365        self.assertEqual(
5366            len(self.selenium.find_elements_by_class_name("select2-selection")),
5367            num_initial_select2_inputs + 4,
5368        )
5369        self.selenium.find_element_by_id(
5370            "id_relatedprepopulated_set-2-1-pubdate"
5371        ).send_keys("1981-08-22")
5372        self.select_option("#id_relatedprepopulated_set-2-1-status", "option one")
5373        self.selenium.find_element_by_id(
5374            "id_relatedprepopulated_set-2-1-name"
5375        ).send_keys(r'a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters')
5376        slug1 = self.selenium.find_element_by_id(
5377            "id_relatedprepopulated_set-2-1-slug1"
5378        ).get_attribute("value")
5379        slug2 = self.selenium.find_element_by_id(
5380            "id_relatedprepopulated_set-2-1-slug2"
5381        ).get_attribute("value")
5382        self.assertEqual(slug1, "tabular-inline-ignored-characters-1981-08-22")
5383        self.assertEqual(slug2, "option-one-tabular-inline-ignored-characters")
5384        # Add an inline without an initial inline.
5385        # The button is outside of the browser frame.
5386        self.selenium.execute_script("window.scrollTo(0, document.body.scrollHeight);")
5387        self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5388            2
5389        ].click()
5390        self.assertEqual(
5391            len(self.selenium.find_elements_by_class_name("select2-selection")),
5392            num_initial_select2_inputs + 6,
5393        )
5394        # Save and check that everything is properly stored in the database
5395        with self.wait_page_loaded():
5396            self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5397        self.assertEqual(MainPrepopulated.objects.all().count(), 1)
5398        MainPrepopulated.objects.get(
5399            name=" this is the mAin nÀMë and it's awεšomeııı",
5400            pubdate="2012-02-18",
5401            status="option two",
5402            slug1="main-name-and-its-awesomeiii-2012-02-18",
5403            slug2="option-two-main-name-and-its-awesomeiii",
5404        )
5405        self.assertEqual(RelatedPrepopulated.objects.all().count(), 4)
5406        RelatedPrepopulated.objects.get(
5407            name=" here is a sŤāÇkeð   inline !  ",
5408            pubdate="2011-12-17",
5409            status="option one",
5410            slug1="here-stacked-inline-2011-12-17",
5411            slug2="option-one-here-stacked-inline",
5412        )
5413        RelatedPrepopulated.objects.get(
5414            # 75 characters in name field
5415            name=" now you haVe anöther   sŤāÇkeð  inline with a very ... loooooooooooooooooo",
5416            pubdate="1999-01-25",
5417            status="option two",
5418            slug1="now-you-have-another-stacked-inline-very-loooooooo",
5419            slug2="option-two-now-you-have-another-stacked-inline-very-looooooo",
5420        )
5421        RelatedPrepopulated.objects.get(
5422            name="And now, with a tÃbűlaŘ inline !!!",
5423            pubdate="1234-12-07",
5424            status="option two",
5425            slug1="and-now-tabular-inline-1234-12-07",
5426            slug2="option-two-and-now-tabular-inline",
5427        )
5428        RelatedPrepopulated.objects.get(
5429            name=r'a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters',
5430            pubdate="1981-08-22",
5431            status="option one",
5432            slug1="tabular-inline-ignored-characters-1981-08-22",
5433            slug2="option-one-tabular-inline-ignored-characters",
5434        )
5435
5436    def test_populate_existing_object(self):
5437        """
5438        The prepopulation works for existing objects too, as long as
5439        the original field is empty (#19082).
5440        """
5441        # Slugs are empty to start with.
5442        item = MainPrepopulated.objects.create(
5443            name=" this is the mAin nÀMë",
5444            pubdate="2012-02-18",
5445            status="option two",
5446            slug1="",
5447            slug2="",
5448        )
5449        self.admin_login(
5450            username="super", password="secret", login_url=reverse("admin:index")
5451        )
5452
5453        object_url = self.live_server_url + reverse(
5454            "admin:admin_views_mainprepopulated_change", args=(item.id,)
5455        )
5456
5457        self.selenium.get(object_url)
5458        self.selenium.find_element_by_id("id_name").send_keys(" the best")
5459
5460        # The slugs got prepopulated since they were originally empty
5461        slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5462        slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5463        self.assertEqual(slug1, "main-name-best-2012-02-18")
5464        self.assertEqual(slug2, "option-two-main-name-best")
5465
5466        # Save the object
5467        with self.wait_page_loaded():
5468            self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5469
5470        self.selenium.get(object_url)
5471        self.selenium.find_element_by_id("id_name").send_keys(" hello")
5472
5473        # The slugs got prepopulated didn't change since they were originally not empty
5474        slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5475        slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5476        self.assertEqual(slug1, "main-name-best-2012-02-18")
5477        self.assertEqual(slug2, "option-two-main-name-best")
5478
5479    def test_collapsible_fieldset(self):
5480        """
5481        The 'collapse' class in fieldsets definition allows to
5482        show/hide the appropriate field section.
5483        """
5484        self.admin_login(
5485            username="super", password="secret", login_url=reverse("admin:index")
5486        )
5487        self.selenium.get(
5488            self.live_server_url + reverse("admin:admin_views_article_add")
5489        )
5490        self.assertFalse(self.selenium.find_element_by_id("id_title").is_displayed())
5491        self.selenium.find_elements_by_link_text("Show")[0].click()
5492        self.assertTrue(self.selenium.find_element_by_id("id_title").is_displayed())
5493        self.assertEqual(
5494            self.selenium.find_element_by_id("fieldsetcollapser0").text, "Hide"
5495        )
5496
5497    def test_first_field_focus(self):
5498        """JavaScript-assisted auto-focus on first usable form field."""
5499        # First form field has a single widget
5500        self.admin_login(
5501            username="super", password="secret", login_url=reverse("admin:index")
5502        )
5503        with self.wait_page_loaded():
5504            self.selenium.get(
5505                self.live_server_url + reverse("admin:admin_views_picture_add")
5506            )
5507        self.assertEqual(
5508            self.selenium.switch_to.active_element,
5509            self.selenium.find_element_by_id("id_name"),
5510        )
5511
5512        # First form field has a MultiWidget
5513        with self.wait_page_loaded():
5514            self.selenium.get(
5515                self.live_server_url + reverse("admin:admin_views_reservation_add")
5516            )
5517        self.assertEqual(
5518            self.selenium.switch_to.active_element,
5519            self.selenium.find_element_by_id("id_start_date_0"),
5520        )
5521
5522    def test_cancel_delete_confirmation(self):
5523        "Cancelling the deletion of an object takes the user back one page."
5524        pizza = Pizza.objects.create(name="Double Cheese")
5525        url = reverse("admin:admin_views_pizza_change", args=(pizza.id,))
5526        full_url = self.live_server_url + url
5527        self.admin_login(
5528            username="super", password="secret", login_url=reverse("admin:index")
5529        )
5530        self.selenium.get(full_url)
5531        self.selenium.find_element_by_class_name("deletelink").click()
5532        # Click 'cancel' on the delete page.
5533        self.selenium.find_element_by_class_name("cancel-link").click()
5534        # Wait until we're back on the change page.
5535        self.wait_for_text("#content h1", "Change pizza")
5536        self.assertEqual(self.selenium.current_url, full_url)
5537        self.assertEqual(Pizza.objects.count(), 1)
5538
5539    def test_cancel_delete_related_confirmation(self):
5540        """
5541        Cancelling the deletion of an object with relations takes the user back
5542        one page.
5543        """
5544        pizza = Pizza.objects.create(name="Double Cheese")
5545        topping1 = Topping.objects.create(name="Cheddar")
5546        topping2 = Topping.objects.create(name="Mozzarella")
5547        pizza.toppings.add(topping1, topping2)
5548        url = reverse("admin:admin_views_pizza_change", args=(pizza.id,))
5549        full_url = self.live_server_url + url
5550        self.admin_login(
5551            username="super", password="secret", login_url=reverse("admin:index")
5552        )
5553        self.selenium.get(full_url)
5554        self.selenium.find_element_by_class_name("deletelink").click()
5555        # Click 'cancel' on the delete page.
5556        self.selenium.find_element_by_class_name("cancel-link").click()
5557        # Wait until we're back on the change page.
5558        self.wait_for_text("#content h1", "Change pizza")
5559        self.assertEqual(self.selenium.current_url, full_url)
5560        self.assertEqual(Pizza.objects.count(), 1)
5561        self.assertEqual(Topping.objects.count(), 2)
5562
5563    def test_list_editable_popups(self):
5564        """
5565        list_editable foreign keys have add/change popups.
5566        """
5567        from selenium.webdriver.support.ui import Select
5568
5569        s1 = Section.objects.create(name="Test section")
5570        Article.objects.create(
5571            title="foo",
5572            content="<p>Middle content</p>",
5573            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
5574            section=s1,
5575        )
5576        self.admin_login(
5577            username="super", password="secret", login_url=reverse("admin:index")
5578        )
5579        self.selenium.get(
5580            self.live_server_url + reverse("admin:admin_views_article_changelist")
5581        )
5582        # Change popup
5583        self.selenium.find_element_by_id("change_id_form-0-section").click()
5584        self.wait_for_and_switch_to_popup()
5585        self.wait_for_text("#content h1", "Change section")
5586        name_input = self.selenium.find_element_by_id("id_name")
5587        name_input.clear()
5588        name_input.send_keys("<i>edited section</i>")
5589        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5590        self.selenium.switch_to.window(self.selenium.window_handles[0])
5591        select = Select(self.selenium.find_element_by_id("id_form-0-section"))
5592        self.assertEqual(select.first_selected_option.text, "<i>edited section</i>")
5593        # Rendered select2 input.
5594        select2_display = self.selenium.find_element_by_class_name(
5595            "select2-selection__rendered"
5596        )
5597        # Clear button (×\n) is included in text.
5598        self.assertEqual(select2_display.text, \n<i>edited section</i>")
5599
5600        # Add popup
5601        self.selenium.find_element_by_id("add_id_form-0-section").click()
5602        self.wait_for_and_switch_to_popup()
5603        self.wait_for_text("#content h1", "Add section")
5604        self.selenium.find_element_by_id("id_name").send_keys("new section")
5605        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5606        self.selenium.switch_to.window(self.selenium.window_handles[0])
5607        select = Select(self.selenium.find_element_by_id("id_form-0-section"))
5608        self.assertEqual(select.first_selected_option.text, "new section")
5609        select2_display = self.selenium.find_element_by_class_name(
5610            "select2-selection__rendered"
5611        )
5612        # Clear button (×\n) is included in text.
5613        self.assertEqual(select2_display.text, \nnew section")
5614
5615    def test_inline_uuid_pk_edit_with_popup(self):
5616        from selenium.webdriver.support.ui import Select
5617
5618        parent = ParentWithUUIDPK.objects.create(title="test")
5619        related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5620        self.admin_login(
5621            username="super", password="secret", login_url=reverse("admin:index")
5622        )
5623        change_url = reverse(
5624            "admin:admin_views_relatedwithuuidpkmodel_change",
5625            args=(related_with_parent.id,),
5626        )
5627        self.selenium.get(self.live_server_url + change_url)
5628        self.selenium.find_element_by_id("change_id_parent").click()
5629        self.wait_for_and_switch_to_popup()
5630        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5631        self.selenium.switch_to.window(self.selenium.window_handles[0])
5632        select = Select(self.selenium.find_element_by_id("id_parent"))
5633        self.assertEqual(select.first_selected_option.text, str(parent.id))
5634        self.assertEqual(
5635            select.first_selected_option.get_attribute("value"), str(parent.id)
5636        )
5637
5638    def test_inline_uuid_pk_add_with_popup(self):
5639        from selenium.webdriver.support.ui import Select
5640
5641        self.admin_login(
5642            username="super", password="secret", login_url=reverse("admin:index")
5643        )
5644        self.selenium.get(
5645            self.live_server_url
5646            + reverse("admin:admin_views_relatedwithuuidpkmodel_add")
5647        )
5648        self.selenium.find_element_by_id("add_id_parent").click()
5649        self.wait_for_and_switch_to_popup()
5650        self.selenium.find_element_by_id("id_title").send_keys("test")
5651        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5652        self.selenium.switch_to.window(self.selenium.window_handles[0])
5653        select = Select(self.selenium.find_element_by_id("id_parent"))
5654        uuid_id = str(ParentWithUUIDPK.objects.first().id)
5655        self.assertEqual(select.first_selected_option.text, uuid_id)
5656        self.assertEqual(select.first_selected_option.get_attribute("value"), uuid_id)
5657
5658    def test_inline_uuid_pk_delete_with_popup(self):
5659        from selenium.webdriver.support.ui import Select
5660
5661        parent = ParentWithUUIDPK.objects.create(title="test")
5662        related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5663        self.admin_login(
5664            username="super", password="secret", login_url=reverse("admin:index")
5665        )
5666        change_url = reverse(
5667            "admin:admin_views_relatedwithuuidpkmodel_change",
5668            args=(related_with_parent.id,),
5669        )
5670        self.selenium.get(self.live_server_url + change_url)
5671        self.selenium.find_element_by_id("delete_id_parent").click()
5672        self.wait_for_and_switch_to_popup()
5673        self.selenium.find_element_by_xpath('//input[@value="Yes, I’m sure"]').click()
5674        self.selenium.switch_to.window(self.selenium.window_handles[0])
5675        select = Select(self.selenium.find_element_by_id("id_parent"))
5676        self.assertEqual(ParentWithUUIDPK.objects.count(), 0)
5677        self.assertEqual(select.first_selected_option.text, "---------")
5678        self.assertEqual(select.first_selected_option.get_attribute("value"), "")
5679
5680    def test_inline_with_popup_cancel_delete(self):
5681        """Clicking ""No, take me back" on a delete popup closes the window."""
5682        parent = ParentWithUUIDPK.objects.create(title="test")
5683        related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5684        self.admin_login(
5685            username="super", password="secret", login_url=reverse("admin:index")
5686        )
5687        change_url = reverse(
5688            "admin:admin_views_relatedwithuuidpkmodel_change",
5689            args=(related_with_parent.id,),
5690        )
5691        self.selenium.get(self.live_server_url + change_url)
5692        self.selenium.find_element_by_id("delete_id_parent").click()
5693        self.wait_for_and_switch_to_popup()
5694        self.selenium.find_element_by_xpath('//a[text()="No, take me back"]').click()
5695        self.selenium.switch_to.window(self.selenium.window_handles[0])
5696        self.assertEqual(len(self.selenium.window_handles), 1)
5697
5698    def test_list_editable_raw_id_fields(self):
5699        parent = ParentWithUUIDPK.objects.create(title="test")
5700        parent2 = ParentWithUUIDPK.objects.create(title="test2")
5701        RelatedWithUUIDPKModel.objects.create(parent=parent)
5702        self.admin_login(
5703            username="super", password="secret", login_url=reverse("admin:index")
5704        )
5705        change_url = reverse(
5706            "admin:admin_views_relatedwithuuidpkmodel_changelist",
5707            current_app=site2.name,
5708        )
5709        self.selenium.get(self.live_server_url + change_url)
5710        self.selenium.find_element_by_id("lookup_id_form-0-parent").click()
5711        self.wait_for_and_switch_to_popup()
5712        # Select "parent2" in the popup.
5713        self.selenium.find_element_by_link_text(str(parent2.pk)).click()
5714        self.selenium.switch_to.window(self.selenium.window_handles[0])
5715        # The newly selected pk should appear in the raw id input.
5716        value = self.selenium.find_element_by_id("id_form-0-parent").get_attribute(
5717            "value"
5718        )
5719        self.assertEqual(value, str(parent2.pk))
5720
5721
5722@override_settings(ROOT_URLCONF="admin_views.urls")
5723class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
5724    @classmethod
5725    def setUpTestData(cls):
5726        cls.superuser = User.objects.create_superuser(
5727            username="super", password="secret", email="super@example.com"
5728        )
5729
5730    def setUp(self):
5731        self.client.force_login(self.superuser)
5732
5733    def test_readonly_get(self):
5734        response = self.client.get(reverse("admin:admin_views_post_add"))
5735        self.assertEqual(response.status_code, 200)
5736        self.assertNotContains(response, 'name="posted"')
5737        # 3 fields + 2 submit buttons + 5 inline management form fields, + 2
5738        # hidden fields for inlines + 1 field for the inline + 2 empty form
5739        self.assertContains(response, "<input", count=15)
5740        self.assertContains(response, formats.localize(datetime.date.today()))
5741        self.assertContains(response, "<label>Awesomeness level:</label>")
5742        self.assertContains(response, "Very awesome.")
5743        self.assertContains(response, "Unknown coolness.")
5744        self.assertContains(response, "foo")
5745
5746        # Multiline text in a readonly field gets <br> tags
5747        self.assertContains(response, "Multiline<br>test<br>string")
5748        self.assertContains(
5749            response,
5750            '<div class="readonly">Multiline<br>html<br>content</div>',
5751            html=True,
5752        )
5753        self.assertContains(response, "InlineMultiline<br>test<br>string")
5754
5755        self.assertContains(
5756            response,
5757            formats.localize(datetime.date.today() - datetime.timedelta(days=7)),
5758        )
5759
5760        self.assertContains(response, '<div class="form-row field-coolness">')
5761        self.assertContains(response, '<div class="form-row field-awesomeness_level">')
5762        self.assertContains(response, '<div class="form-row field-posted">')
5763        self.assertContains(response, '<div class="form-row field-value">')
5764        self.assertContains(response, '<div class="form-row">')
5765        self.assertContains(response, '<div class="help">', 3)
5766        self.assertContains(
5767            response,
5768            '<div class="help">Some help text for the title (with unicode ŠĐĆŽćžšđ)</div>',
5769            html=True,
5770        )
5771        self.assertContains(
5772            response,
5773            '<div class="help">Some help text for the content (with unicode ŠĐĆŽćžšđ)</div>',
5774            html=True,
5775        )
5776        self.assertContains(
5777            response,
5778            '<div class="help">Some help text for the date (with unicode ŠĐĆŽćžšđ)</div>',
5779            html=True,
5780        )
5781
5782        p = Post.objects.create(
5783            title="I worked on readonly_fields", content="Its good stuff"
5784        )
5785        response = self.client.get(
5786            reverse("admin:admin_views_post_change", args=(p.pk,))
5787        )
5788        self.assertContains(response, "%d amount of cool" % p.pk)
5789
5790    def test_readonly_text_field(self):
5791        p = Post.objects.create(
5792            title="Readonly test",
5793            content="test",
5794            readonly_content="test\r\n\r\ntest\r\n\r\ntest\r\n\r\ntest",
5795        )
5796        Link.objects.create(
5797            url="http://www.djangoproject.com",
5798            post=p,
5799            readonly_link_content="test\r\nlink",
5800        )
5801        response = self.client.get(
5802            reverse("admin:admin_views_post_change", args=(p.pk,))
5803        )
5804        # Checking readonly field.
5805        self.assertContains(response, "test<br><br>test<br><br>test<br><br>test")
5806        # Checking readonly field in inline.
5807        self.assertContains(response, "test<br>link")
5808
5809    def test_readonly_post(self):
5810        data = {
5811            "title": "Django Got Readonly Fields",
5812            "content": "This is an incredible development.",
5813            "link_set-TOTAL_FORMS": "1",
5814            "link_set-INITIAL_FORMS": "0",
5815            "link_set-MAX_NUM_FORMS": "0",
5816        }
5817        response = self.client.post(reverse("admin:admin_views_post_add"), data)
5818        self.assertEqual(response.status_code, 302)
5819        self.assertEqual(Post.objects.count(), 1)
5820        p = Post.objects.get()
5821        self.assertEqual(p.posted, datetime.date.today())
5822
5823        data["posted"] = "10-8-1990"  # some date that's not today
5824        response = self.client.post(reverse("admin:admin_views_post_add"), data)
5825        self.assertEqual(response.status_code, 302)
5826        self.assertEqual(Post.objects.count(), 2)
5827        p = Post.objects.order_by("-id")[0]
5828        self.assertEqual(p.posted, datetime.date.today())
5829
5830    def test_readonly_manytomany(self):
5831        "Regression test for #13004"
5832        response = self.client.get(reverse("admin:admin_views_pizza_add"))
5833        self.assertEqual(response.status_code, 200)
5834
5835    def test_user_password_change_limited_queryset(self):
5836        su = User.objects.filter(is_superuser=True)[0]
5837        response = self.client.get(
5838            reverse("admin2:auth_user_password_change", args=(su.pk,))
5839        )
5840        self.assertEqual(response.status_code, 404)
5841
5842    def test_change_form_renders_correct_null_choice_value(self):
5843        """
5844        Regression test for #17911.
5845        """
5846        choice = Choice.objects.create(choice=None)
5847        response = self.client.get(
5848            reverse("admin:admin_views_choice_change", args=(choice.pk,))
5849        )
5850        self.assertContains(
5851            response, '<div class="readonly">No opinion</div>', html=True
5852        )
5853
5854    def test_readonly_manytomany_backwards_ref(self):
5855        """
5856        Regression test for #16433 - backwards references for related objects
5857        broke if the related field is read-only due to the help_text attribute
5858        """
5859        topping = Topping.objects.create(name="Salami")
5860        pizza = Pizza.objects.create(name="Americano")
5861        pizza.toppings.add(topping)
5862        response = self.client.get(reverse("admin:admin_views_topping_add"))
5863        self.assertEqual(response.status_code, 200)
5864
5865    def test_readonly_manytomany_forwards_ref(self):
5866        topping = Topping.objects.create(name="Salami")
5867        pizza = Pizza.objects.create(name="Americano")
5868        pizza.toppings.add(topping)
5869        response = self.client.get(
5870            reverse("admin:admin_views_pizza_change", args=(pizza.pk,))
5871        )
5872        self.assertContains(response, "<label>Toppings:</label>", html=True)
5873        self.assertContains(response, '<div class="readonly">Salami</div>', html=True)
5874
5875    def test_readonly_onetoone_backwards_ref(self):
5876        """
5877        Can reference a reverse OneToOneField in ModelAdmin.readonly_fields.
5878        """
5879        v1 = Villain.objects.create(name="Adam")
5880        pl = Plot.objects.create(name="Test Plot", team_leader=v1, contact=v1)
5881        pd = PlotDetails.objects.create(details="Brand New Plot", plot=pl)
5882
5883        response = self.client.get(
5884            reverse("admin:admin_views_plotproxy_change", args=(pl.pk,))
5885        )
5886        field = self.get_admin_readonly_field(response, "plotdetails")
5887        self.assertEqual(field.contents(), "Brand New Plot")
5888
5889        # The reverse relation also works if the OneToOneField is null.
5890        pd.plot = None
5891        pd.save()
5892
5893        response = self.client.get(
5894            reverse("admin:admin_views_plotproxy_change", args=(pl.pk,))
5895        )
5896        field = self.get_admin_readonly_field(response, "plotdetails")
5897        self.assertEqual(field.contents(), "-")  # default empty value
5898
5899    def test_readonly_field_overrides(self):
5900        """
5901        Regression test for #22087 - ModelForm Meta overrides are ignored by
5902        AdminReadonlyField
5903        """
5904        p = FieldOverridePost.objects.create(title="Test Post", content="Test Content")
5905        response = self.client.get(
5906            reverse("admin:admin_views_fieldoverridepost_change", args=(p.pk,))
5907        )
5908        self.assertContains(
5909            response, '<div class="help">Overridden help text for the date</div>'
5910        )
5911        self.assertContains(
5912            response,
5913            '<label for="id_public">Overridden public label:</label>',
5914            html=True,
5915        )
5916        self.assertNotContains(
5917            response, "Some help text for the date (with unicode ŠĐĆŽćžšđ)"
5918        )
5919
5920    def test_correct_autoescaping(self):
5921        """
5922        Make sure that non-field readonly elements are properly autoescaped (#24461)
5923        """
5924        section = Section.objects.create(name="<a>evil</a>")
5925        response = self.client.get(
5926            reverse("admin:admin_views_section_change", args=(section.pk,))
5927        )
5928        self.assertNotContains(response, "<a>evil</a>", status_code=200)
5929        self.assertContains(response, "&lt;a&gt;evil&lt;/a&gt;", status_code=200)
5930
5931    def test_label_suffix_translated(self):
5932        pizza = Pizza.objects.create(name="Americano")
5933        url = reverse("admin:admin_views_pizza_change", args=(pizza.pk,))
5934        with self.settings(LANGUAGE_CODE="fr"):
5935            response = self.client.get(url)
5936        self.assertContains(response, "<label>Toppings\u00A0:</label>", html=True)
5937
5938
5939@override_settings(ROOT_URLCONF="admin_views.urls")
5940class LimitChoicesToInAdminTest(TestCase):
5941    @classmethod
5942    def setUpTestData(cls):
5943        cls.superuser = User.objects.create_superuser(
5944            username="super", password="secret", email="super@example.com"
5945        )
5946
5947    def setUp(self):
5948        self.client.force_login(self.superuser)
5949
5950    def test_limit_choices_to_as_callable(self):
5951        """Test for ticket 2445 changes to admin."""
5952        threepwood = Character.objects.create(
5953            username="threepwood",
5954            last_action=datetime.datetime.today() + datetime.timedelta(days=1),
5955        )
5956        marley = Character.objects.create(
5957            username="marley",
5958            last_action=datetime.datetime.today() - datetime.timedelta(days=1),
5959        )
5960        response = self.client.get(reverse("admin:admin_views_stumpjoke_add"))
5961        # The allowed option should appear twice; the limited option should not appear.
5962        self.assertContains(response, threepwood.username, count=2)
5963        self.assertNotContains(response, marley.username)
5964
5965
5966@override_settings(ROOT_URLCONF="admin_views.urls")
5967class RawIdFieldsTest(TestCase):
5968    @classmethod
5969    def setUpTestData(cls):
5970        cls.superuser = User.objects.create_superuser(
5971            username="super", password="secret", email="super@example.com"
5972        )
5973
5974    def setUp(self):
5975        self.client.force_login(self.superuser)
5976
5977    def test_limit_choices_to(self):
5978        """Regression test for 14880"""
5979        actor = Actor.objects.create(name="Palin", age=27)
5980        Inquisition.objects.create(expected=True, leader=actor, country="England")
5981        Inquisition.objects.create(expected=False, leader=actor, country="Spain")
5982        response = self.client.get(reverse("admin:admin_views_sketch_add"))
5983        # Find the link
5984        m = re.search(
5985            br'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content
5986        )
5987        self.assertTrue(m)  # Got a match
5988        popup_url = m.groups()[0].decode().replace("&amp;", "&")
5989
5990        # Handle relative links
5991        popup_url = urljoin(response.request["PATH_INFO"], popup_url)
5992        # Get the popup and verify the correct objects show up in the resulting
5993        # page. This step also tests integers, strings and booleans in the
5994        # lookup query string; in model we define inquisition field to have a
5995        # limit_choices_to option that includes a filter on a string field
5996        # (inquisition__actor__name), a filter on an integer field
5997        # (inquisition__actor__age), and a filter on a boolean field
5998        # (inquisition__expected).
5999        response2 = self.client.get(popup_url)
6000        self.assertContains(response2, "Spain")
6001        self.assertNotContains(response2, "England")
6002
6003    def test_limit_choices_to_isnull_false(self):
6004        """Regression test for 20182"""
6005        Actor.objects.create(name="Palin", age=27)
6006        Actor.objects.create(name="Kilbraken", age=50, title="Judge")
6007        response = self.client.get(reverse("admin:admin_views_sketch_add"))
6008        # Find the link
6009        m = re.search(
6010            br'<a href="([^"]*)"[^>]* id="lookup_id_defendant0"', response.content
6011        )
6012        self.assertTrue(m)  # Got a match
6013        popup_url = m.groups()[0].decode().replace("&amp;", "&")
6014
6015        # Handle relative links
6016        popup_url = urljoin(response.request["PATH_INFO"], popup_url)
6017        # Get the popup and verify the correct objects show up in the resulting
6018        # page. This step tests field__isnull=0 gets parsed correctly from the
6019        # lookup query string; in model we define defendant0 field to have a
6020        # limit_choices_to option that includes "actor__title__isnull=False".
6021        response2 = self.client.get(popup_url)
6022        self.assertContains(response2, "Kilbraken")
6023        self.assertNotContains(response2, "Palin")
6024
6025    def test_limit_choices_to_isnull_true(self):
6026        """Regression test for 20182"""
6027        Actor.objects.create(name="Palin", age=27)
6028        Actor.objects.create(name="Kilbraken", age=50, title="Judge")
6029        response = self.client.get(reverse("admin:admin_views_sketch_add"))
6030        # Find the link
6031        m = re.search(
6032            br'<a href="([^"]*)"[^>]* id="lookup_id_defendant1"', response.content
6033        )
6034        self.assertTrue(m)  # Got a match
6035        popup_url = m.groups()[0].decode().replace("&amp;", "&")
6036
6037        # Handle relative links
6038        popup_url = urljoin(response.request["PATH_INFO"], popup_url)
6039        # Get the popup and verify the correct objects show up in the resulting
6040        # page. This step tests field__isnull=1 gets parsed correctly from the
6041        # lookup query string; in model we define defendant1 field to have a
6042        # limit_choices_to option that includes "actor__title__isnull=True".
6043        response2 = self.client.get(popup_url)
6044        self.assertNotContains(response2, "Kilbraken")
6045        self.assertContains(response2, "Palin")
6046
6047    def test_list_display_method_same_name_as_reverse_accessor(self):
6048        """
6049        Should be able to use a ModelAdmin method in list_display that has the
6050        same name as a reverse model field ("sketch" in this case).
6051        """
6052        actor = Actor.objects.create(name="Palin", age=27)
6053        Inquisition.objects.create(expected=True, leader=actor, country="England")
6054        response = self.client.get(reverse("admin:admin_views_inquisition_changelist"))
6055        self.assertContains(response, "list-display-sketch")
6056
6057
6058@override_settings(ROOT_URLCONF="admin_views.urls")
6059class UserAdminTest(TestCase):
6060    """
6061    Tests user CRUD functionality.
6062    """
6063
6064    @classmethod
6065    def setUpTestData(cls):
6066        cls.superuser = User.objects.create_superuser(
6067            username="super", password="secret", email="super@example.com"
6068        )
6069        cls.adduser = User.objects.create_user(
6070            username="adduser", password="secret", is_staff=True
6071        )
6072        cls.changeuser = User.objects.create_user(
6073            username="changeuser", password="secret", is_staff=True
6074        )
6075        cls.s1 = Section.objects.create(name="Test section")
6076        cls.a1 = Article.objects.create(
6077            content="<p>Middle content</p>",
6078            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
6079            section=cls.s1,
6080        )
6081        cls.a2 = Article.objects.create(
6082            content="<p>Oldest content</p>",
6083            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
6084            section=cls.s1,
6085        )
6086        cls.a3 = Article.objects.create(
6087            content="<p>Newest content</p>",
6088            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
6089            section=cls.s1,
6090        )
6091        cls.p1 = PrePopulatedPost.objects.create(
6092            title="A Long Title", published=True, slug="a-long-title"
6093        )
6094
6095        cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
6096        cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
6097        cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
6098
6099    def setUp(self):
6100        self.client.force_login(self.superuser)
6101
6102    def test_save_button(self):
6103        user_count = User.objects.count()
6104        response = self.client.post(
6105            reverse("admin:auth_user_add"),
6106            {
6107                "username": "newuser",
6108                "password1": "newpassword",
6109                "password2": "newpassword",
6110            },
6111        )
6112        new_user = User.objects.get(username="newuser")
6113        self.assertRedirects(
6114            response, reverse("admin:auth_user_change", args=(new_user.pk,))
6115        )
6116        self.assertEqual(User.objects.count(), user_count + 1)
6117        self.assertTrue(new_user.has_usable_password())
6118
6119    def test_save_continue_editing_button(self):
6120        user_count = User.objects.count()
6121        response = self.client.post(
6122            reverse("admin:auth_user_add"),
6123            {
6124                "username": "newuser",
6125                "password1": "newpassword",
6126                "password2": "newpassword",
6127                "_continue": "1",
6128            },
6129        )
6130        new_user = User.objects.get(username="newuser")
6131        new_user_url = reverse("admin:auth_user_change", args=(new_user.pk,))
6132        self.assertRedirects(response, new_user_url, fetch_redirect_response=False)
6133        self.assertEqual(User.objects.count(), user_count + 1)
6134        self.assertTrue(new_user.has_usable_password())
6135        response = self.client.get(new_user_url)
6136        self.assertContains(
6137            response,
6138            '<li class="success">The user “<a href="%s">'
6139            "%s</a>” was added successfully. You may edit it again below.</li>"
6140            % (new_user_url, new_user),
6141            html=True,
6142        )
6143
6144    def test_password_mismatch(self):
6145        response = self.client.post(
6146            reverse("admin:auth_user_add"),
6147            {
6148                "username": "newuser",
6149                "password1": "newpassword",
6150                "password2": "mismatch",
6151            },
6152        )
6153        self.assertEqual(response.status_code, 200)
6154        self.assertFormError(response, "adminform", "password", [])
6155        self.assertFormError(
6156            response,
6157            "adminform",
6158            "password2",
6159            ["The two password fields didn’t match."],
6160        )
6161
6162    def test_user_fk_add_popup(self):
6163        """User addition through a FK popup should return the appropriate JavaScript response."""
6164        response = self.client.get(reverse("admin:admin_views_album_add"))
6165        self.assertContains(response, reverse("admin:auth_user_add"))
6166        self.assertContains(
6167            response,
6168            'class="related-widget-wrapper-link add-related" id="add_id_owner"',
6169        )
6170        response = self.client.get(reverse("admin:auth_user_add") + "?_popup=1")
6171        self.assertNotContains(response, 'name="_continue"')
6172        self.assertNotContains(response, 'name="_addanother"')
6173        data = {
6174            "username": "newuser",
6175            "password1": "newpassword",
6176            "password2": "newpassword",
6177            "_popup": "1",
6178            "_save": "1",
6179        }
6180        response = self.client.post(
6181            reverse("admin:auth_user_add") + "?_popup=1", data, follow=True
6182        )
6183        self.assertContains(response, "&quot;obj&quot;: &quot;newuser&quot;")
6184
6185    def test_user_fk_change_popup(self):
6186        """User change through a FK popup should return the appropriate JavaScript response."""
6187        response = self.client.get(reverse("admin:admin_views_album_add"))
6188        self.assertContains(
6189            response, reverse("admin:auth_user_change", args=("__fk__",))
6190        )
6191        self.assertContains(
6192            response,
6193            'class="related-widget-wrapper-link change-related" id="change_id_owner"',
6194        )
6195        user = User.objects.get(username="changeuser")
6196        url = reverse("admin:auth_user_change", args=(user.pk,)) + "?_popup=1"
6197        response = self.client.get(url)
6198        self.assertNotContains(response, 'name="_continue"')
6199        self.assertNotContains(response, 'name="_addanother"')
6200        data = {
6201            "username": "newuser",
6202            "password1": "newpassword",
6203            "password2": "newpassword",
6204            "last_login_0": "2007-05-30",
6205            "last_login_1": "13:20:10",
6206            "date_joined_0": "2007-05-30",
6207            "date_joined_1": "13:20:10",
6208            "_popup": "1",
6209            "_save": "1",
6210        }
6211        response = self.client.post(url, data, follow=True)
6212        self.assertContains(response, "&quot;obj&quot;: &quot;newuser&quot;")
6213        self.assertContains(response, "&quot;action&quot;: &quot;change&quot;")
6214
6215    def test_user_fk_delete_popup(self):
6216        """User deletion through a FK popup should return the appropriate JavaScript response."""
6217        response = self.client.get(reverse("admin:admin_views_album_add"))
6218        self.assertContains(
6219            response, reverse("admin:auth_user_delete", args=("__fk__",))
6220        )
6221        self.assertContains(
6222            response,
6223            'class="related-widget-wrapper-link change-related" id="change_id_owner"',
6224        )
6225        user = User.objects.get(username="changeuser")
6226        url = reverse("admin:auth_user_delete", args=(user.pk,)) + "?_popup=1"
6227        response = self.client.get(url)
6228        self.assertEqual(response.status_code, 200)
6229        data = {
6230            "post": "yes",
6231            "_popup": "1",
6232        }
6233        response = self.client.post(url, data, follow=True)
6234        self.assertContains(response, "&quot;action&quot;: &quot;delete&quot;")
6235
6236    def test_save_add_another_button(self):
6237        user_count = User.objects.count()
6238        response = self.client.post(
6239            reverse("admin:auth_user_add"),
6240            {
6241                "username": "newuser",
6242                "password1": "newpassword",
6243                "password2": "newpassword",
6244                "_addanother": "1",
6245            },
6246        )
6247        new_user = User.objects.order_by("-id")[0]
6248        self.assertRedirects(response, reverse("admin:auth_user_add"))
6249        self.assertEqual(User.objects.count(), user_count + 1)
6250        self.assertTrue(new_user.has_usable_password())
6251
6252    def test_user_permission_performance(self):
6253        u = User.objects.all()[0]
6254
6255        # Don't depend on a warm cache, see #17377.
6256        ContentType.objects.clear_cache()
6257
6258        with self.assertNumQueries(10):
6259            response = self.client.get(reverse("admin:auth_user_change", args=(u.pk,)))
6260            self.assertEqual(response.status_code, 200)
6261
6262    def test_form_url_present_in_context(self):
6263        u = User.objects.all()[0]
6264        response = self.client.get(
6265            reverse("admin3:auth_user_password_change", args=(u.pk,))
6266        )
6267        self.assertEqual(response.status_code, 200)
6268        self.assertEqual(response.context["form_url"], "pony")
6269
6270
6271@override_settings(ROOT_URLCONF="admin_views.urls")
6272class GroupAdminTest(TestCase):
6273    """
6274    Tests group CRUD functionality.
6275    """
6276
6277    @classmethod
6278    def setUpTestData(cls):
6279        cls.superuser = User.objects.create_superuser(
6280            username="super", password="secret", email="super@example.com"
6281        )
6282
6283    def setUp(self):
6284        self.client.force_login(self.superuser)
6285
6286    def test_save_button(self):
6287        group_count = Group.objects.count()
6288        response = self.client.post(
6289            reverse("admin:auth_group_add"), {"name": "newgroup",}
6290        )
6291
6292        Group.objects.order_by("-id")[0]
6293        self.assertRedirects(response, reverse("admin:auth_group_changelist"))
6294        self.assertEqual(Group.objects.count(), group_count + 1)
6295
6296    def test_group_permission_performance(self):
6297        g = Group.objects.create(name="test_group")
6298
6299        # Ensure no queries are skipped due to cached content type for Group.
6300        ContentType.objects.clear_cache()
6301
6302        with self.assertNumQueries(8):
6303            response = self.client.get(reverse("admin:auth_group_change", args=(g.pk,)))
6304            self.assertEqual(response.status_code, 200)
6305
6306
6307@override_settings(ROOT_URLCONF="admin_views.urls")
6308class CSSTest(TestCase):
6309    @classmethod
6310    def setUpTestData(cls):
6311        cls.superuser = User.objects.create_superuser(
6312            username="super", password="secret", email="super@example.com"
6313        )
6314        cls.s1 = Section.objects.create(name="Test section")
6315        cls.a1 = Article.objects.create(
6316            content="<p>Middle content</p>",
6317            date=datetime.datetime(2008, 3, 18, 11, 54, 58),
6318            section=cls.s1,
6319        )
6320        cls.a2 = Article.objects.create(
6321            content="<p>Oldest content</p>",
6322            date=datetime.datetime(2000, 3, 18, 11, 54, 58),
6323            section=cls.s1,
6324        )
6325        cls.a3 = Article.objects.create(
6326            content="<p>Newest content</p>",
6327            date=datetime.datetime(2009, 3, 18, 11, 54, 58),
6328            section=cls.s1,
6329        )
6330        cls.p1 = PrePopulatedPost.objects.create(
6331            title="A Long Title", published=True, slug="a-long-title"
6332        )
6333
6334    def setUp(self):
6335        self.client.force_login(self.superuser)
6336
6337    def test_field_prefix_css_classes(self):
6338        """
6339        Fields have a CSS class name with a 'field-' prefix.
6340        """
6341        response = self.client.get(reverse("admin:admin_views_post_add"))
6342
6343        # The main form
6344        self.assertContains(response, 'class="form-row field-title"')
6345        self.assertContains(response, 'class="form-row field-content"')
6346        self.assertContains(response, 'class="form-row field-public"')
6347        self.assertContains(response, 'class="form-row field-awesomeness_level"')
6348        self.assertContains(response, 'class="form-row field-coolness"')
6349        self.assertContains(response, 'class="form-row field-value"')
6350        self.assertContains(response, 'class="form-row"')  # The lambda function
6351
6352        # The tabular inline
6353        self.assertContains(response, '<td class="field-url">')
6354        self.assertContains(response, '<td class="field-posted">')
6355
6356    def test_index_css_classes(self):
6357        """
6358        CSS class names are used for each app and model on the admin index
6359        pages (#17050).
6360        """
6361        # General index page
6362        response = self.client.get(reverse("admin:index"))
6363        self.assertContains(response, '<div class="app-admin_views module">')
6364        self.assertContains(response, '<tr class="model-actor">')
6365        self.assertContains(response, '<tr class="model-album">')
6366
6367        # App index page
6368        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6369        self.assertContains(response, '<div class="app-admin_views module">')
6370        self.assertContains(response, '<tr class="model-actor">')
6371        self.assertContains(response, '<tr class="model-album">')
6372
6373    def test_app_model_in_form_body_class(self):
6374        """
6375        Ensure app and model tag are correctly read by change_form template
6376        """
6377        response = self.client.get(reverse("admin:admin_views_section_add"))
6378        self.assertContains(response, '<body class=" app-admin_views model-section ')
6379
6380    def test_app_model_in_list_body_class(self):
6381        """
6382        Ensure app and model tag are correctly read by change_list template
6383        """
6384        response = self.client.get(reverse("admin:admin_views_section_changelist"))
6385        self.assertContains(response, '<body class=" app-admin_views model-section ')
6386
6387    def test_app_model_in_delete_confirmation_body_class(self):
6388        """
6389        Ensure app and model tag are correctly read by delete_confirmation
6390        template
6391        """
6392        response = self.client.get(
6393            reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
6394        )
6395        self.assertContains(response, '<body class=" app-admin_views model-section ')
6396
6397    def test_app_model_in_app_index_body_class(self):
6398        """
6399        Ensure app and model tag are correctly read by app_index template
6400        """
6401        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6402        self.assertContains(response, '<body class=" dashboard app-admin_views')
6403
6404    def test_app_model_in_delete_selected_confirmation_body_class(self):
6405        """
6406        Ensure app and model tag are correctly read by
6407        delete_selected_confirmation template
6408        """
6409        action_data = {
6410            ACTION_CHECKBOX_NAME: [self.s1.pk],
6411            "action": "delete_selected",
6412            "index": 0,
6413        }
6414        response = self.client.post(
6415            reverse("admin:admin_views_section_changelist"), action_data
6416        )
6417        self.assertContains(response, '<body class=" app-admin_views model-section ')
6418
6419    def test_changelist_field_classes(self):
6420        """
6421        Cells of the change list table should contain the field name in their class attribute
6422        Refs #11195.
6423        """
6424        Podcast.objects.create(name="Django Dose", release_date=datetime.date.today())
6425        response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6426        self.assertContains(response, '<th class="field-name">')
6427        self.assertContains(response, '<td class="field-release_date nowrap">')
6428        self.assertContains(response, '<td class="action-checkbox">')
6429
6430
6431try:
6432    import docutils
6433except ImportError:
6434    docutils = None
6435
6436
6437@unittest.skipUnless(docutils, "no docutils installed.")
6438@override_settings(ROOT_URLCONF="admin_views.urls")
6439@modify_settings(
6440    INSTALLED_APPS={"append": ["django.contrib.admindocs", "django.contrib.flatpages"]}
6441)
6442class AdminDocsTest(TestCase):
6443    @classmethod
6444    def setUpTestData(cls):
6445        cls.superuser = User.objects.create_superuser(
6446            username="super", password="secret", email="super@example.com"
6447        )
6448
6449    def setUp(self):
6450        self.client.force_login(self.superuser)
6451
6452    def test_tags(self):
6453        response = self.client.get(reverse("django-admindocs-tags"))
6454
6455        # The builtin tag group exists
6456        self.assertContains(response, "<h2>Built-in tags</h2>", count=2, html=True)
6457
6458        # A builtin tag exists in both the index and detail
6459        self.assertContains(
6460            response, '<h3 id="built_in-autoescape">autoescape</h3>', html=True
6461        )
6462        self.assertContains(
6463            response,
6464            '<li><a href="#built_in-autoescape">autoescape</a></li>',
6465            html=True,
6466        )
6467
6468        # An app tag exists in both the index and detail
6469        self.assertContains(
6470            response, '<h3 id="flatpages-get_flatpages">get_flatpages</h3>', html=True
6471        )
6472        self.assertContains(
6473            response,
6474            '<li><a href="#flatpages-get_flatpages">get_flatpages</a></li>',
6475            html=True,
6476        )
6477
6478        # The admin list tag group exists
6479        self.assertContains(response, "<h2>admin_list</h2>", count=2, html=True)
6480
6481        # An admin list tag exists in both the index and detail
6482        self.assertContains(
6483            response, '<h3 id="admin_list-admin_actions">admin_actions</h3>', html=True
6484        )
6485        self.assertContains(
6486            response,
6487            '<li><a href="#admin_list-admin_actions">admin_actions</a></li>',
6488            html=True,
6489        )
6490
6491    def test_filters(self):
6492        response = self.client.get(reverse("django-admindocs-filters"))
6493
6494        # The builtin filter group exists
6495        self.assertContains(response, "<h2>Built-in filters</h2>", count=2, html=True)
6496
6497        # A builtin filter exists in both the index and detail
6498        self.assertContains(response, '<h3 id="built_in-add">add</h3>', html=True)
6499        self.assertContains(
6500            response, '<li><a href="#built_in-add">add</a></li>', html=True
6501        )
6502
6503
6504@override_settings(
6505    ROOT_URLCONF="admin_views.urls",
6506    TEMPLATES=[
6507        {
6508            "BACKEND": "django.template.backends.django.DjangoTemplates",
6509            "APP_DIRS": True,
6510            "OPTIONS": {
6511                "context_processors": [
6512                    "django.template.context_processors.debug",
6513                    "django.template.context_processors.request",
6514                    "django.contrib.auth.context_processors.auth",
6515                    "django.contrib.messages.context_processors.messages",
6516                ],
6517            },
6518        }
6519    ],
6520    USE_I18N=False,
6521)
6522class ValidXHTMLTests(TestCase):
6523    @classmethod
6524    def setUpTestData(cls):
6525        cls.superuser = User.objects.create_superuser(
6526            username="super", password="secret", email="super@example.com"
6527        )
6528
6529    def setUp(self):
6530        self.client.force_login(self.superuser)
6531
6532    def test_lang_name_present(self):
6533        response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6534        self.assertNotContains(response, ' lang=""')
6535        self.assertNotContains(response, ' xml:lang=""')
6536
6537
6538@override_settings(
6539    ROOT_URLCONF="admin_views.urls", USE_THOUSAND_SEPARATOR=True, USE_L10N=True
6540)
6541class DateHierarchyTests(TestCase):
6542    @classmethod
6543    def setUpTestData(cls):
6544        cls.superuser = User.objects.create_superuser(
6545            username="super", password="secret", email="super@example.com"
6546        )
6547
6548    def setUp(self):
6549        self.client.force_login(self.superuser)
6550
6551    def assert_non_localized_year(self, response, year):
6552        """
6553        The year is not localized with USE_THOUSAND_SEPARATOR (#15234).
6554        """
6555        self.assertNotContains(response, formats.number_format(year))
6556
6557    def assert_contains_year_link(self, response, date):
6558        self.assertContains(response, '?release_date__year=%d"' % (date.year,))
6559
6560    def assert_contains_month_link(self, response, date):
6561        self.assertContains(
6562            response,
6563            '?release_date__month=%d&amp;release_date__year=%d"'
6564            % (date.month, date.year),
6565        )
6566
6567    def assert_contains_day_link(self, response, date):
6568        self.assertContains(
6569            response,
6570            "?release_date__day=%d&amp;"
6571            'release_date__month=%d&amp;release_date__year=%d"'
6572            % (date.day, date.month, date.year),
6573        )
6574
6575    def test_empty(self):
6576        """
6577        No date hierarchy links display with empty changelist.
6578        """
6579        response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6580        self.assertNotContains(response, "release_date__year=")
6581        self.assertNotContains(response, "release_date__month=")
6582        self.assertNotContains(response, "release_date__day=")
6583
6584    def test_single(self):
6585        """
6586        Single day-level date hierarchy appears for single object.
6587        """
6588        DATE = datetime.date(2000, 6, 30)
6589        Podcast.objects.create(release_date=DATE)
6590        url = reverse("admin:admin_views_podcast_changelist")
6591        response = self.client.get(url)
6592        self.assert_contains_day_link(response, DATE)
6593        self.assert_non_localized_year(response, 2000)
6594
6595    def test_within_month(self):
6596        """
6597        day-level links appear for changelist within single month.
6598        """
6599        DATES = (
6600            datetime.date(2000, 6, 30),
6601            datetime.date(2000, 6, 15),
6602            datetime.date(2000, 6, 3),
6603        )
6604        for date in DATES:
6605            Podcast.objects.create(release_date=date)
6606        url = reverse("admin:admin_views_podcast_changelist")
6607        response = self.client.get(url)
6608        for date in DATES:
6609            self.assert_contains_day_link(response, date)
6610        self.assert_non_localized_year(response, 2000)
6611
6612    def test_within_year(self):
6613        """
6614        month-level links appear for changelist within single year.
6615        """
6616        DATES = (
6617            datetime.date(2000, 1, 30),
6618            datetime.date(2000, 3, 15),
6619            datetime.date(2000, 5, 3),
6620        )
6621        for date in DATES:
6622            Podcast.objects.create(release_date=date)
6623        url = reverse("admin:admin_views_podcast_changelist")
6624        response = self.client.get(url)
6625        # no day-level links
6626        self.assertNotContains(response, "release_date__day=")
6627        for date in DATES:
6628            self.assert_contains_month_link(response, date)
6629        self.assert_non_localized_year(response, 2000)
6630
6631    def test_multiple_years(self):
6632        """
6633        year-level links appear for year-spanning changelist.
6634        """
6635        DATES = (
6636            datetime.date(2001, 1, 30),
6637            datetime.date(2003, 3, 15),
6638            datetime.date(2005, 5, 3),
6639        )
6640        for date in DATES:
6641            Podcast.objects.create(release_date=date)
6642        response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6643        # no day/month-level links
6644        self.assertNotContains(response, "release_date__day=")
6645        self.assertNotContains(response, "release_date__month=")
6646        for date in DATES:
6647            self.assert_contains_year_link(response, date)
6648
6649        # and make sure GET parameters still behave correctly
6650        for date in DATES:
6651            url = "%s?release_date__year=%d" % (
6652                reverse("admin:admin_views_podcast_changelist"),
6653                date.year,
6654            )
6655            response = self.client.get(url)
6656            self.assert_contains_month_link(response, date)
6657            self.assert_non_localized_year(response, 2000)
6658            self.assert_non_localized_year(response, 2003)
6659            self.assert_non_localized_year(response, 2005)
6660
6661            url = "%s?release_date__year=%d&release_date__month=%d" % (
6662                reverse("admin:admin_views_podcast_changelist"),
6663                date.year,
6664                date.month,
6665            )
6666            response = self.client.get(url)
6667            self.assert_contains_day_link(response, date)
6668            self.assert_non_localized_year(response, 2000)
6669            self.assert_non_localized_year(response, 2003)
6670            self.assert_non_localized_year(response, 2005)
6671
6672    def test_related_field(self):
6673        questions_data = (
6674            # (posted data, number of answers),
6675            (datetime.date(2001, 1, 30), 0),
6676            (datetime.date(2003, 3, 15), 1),
6677            (datetime.date(2005, 5, 3), 2),
6678        )
6679        for date, answer_count in questions_data:
6680            question = Question.objects.create(posted=date)
6681            for i in range(answer_count):
6682                question.answer_set.create()
6683
6684        response = self.client.get(reverse("admin:admin_views_answer_changelist"))
6685        for date, answer_count in questions_data:
6686            link = '?question__posted__year=%d"' % (date.year,)
6687            if answer_count > 0:
6688                self.assertContains(response, link)
6689            else:
6690                self.assertNotContains(response, link)
6691
6692
6693@override_settings(ROOT_URLCONF="admin_views.urls")
6694class AdminCustomSaveRelatedTests(TestCase):
6695    """
6696    One can easily customize the way related objects are saved.
6697    Refs #16115.
6698    """
6699
6700    @classmethod
6701    def setUpTestData(cls):
6702        cls.superuser = User.objects.create_superuser(
6703            username="super", password="secret", email="super@example.com"
6704        )
6705
6706    def setUp(self):
6707        self.client.force_login(self.superuser)
6708
6709    def test_should_be_able_to_edit_related_objects_on_add_view(self):
6710        post = {
6711            "child_set-TOTAL_FORMS": "3",
6712            "child_set-INITIAL_FORMS": "0",
6713            "name": "Josh Stone",
6714            "child_set-0-name": "Paul",
6715            "child_set-1-name": "Catherine",
6716        }
6717        self.client.post(reverse("admin:admin_views_parent_add"), post)
6718        self.assertEqual(1, Parent.objects.count())
6719        self.assertEqual(2, Child.objects.count())
6720
6721        children_names = list(
6722            Child.objects.order_by("name").values_list("name", flat=True)
6723        )
6724
6725        self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6726        self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6727
6728    def test_should_be_able_to_edit_related_objects_on_change_view(self):
6729        parent = Parent.objects.create(name="Josh Stone")
6730        paul = Child.objects.create(parent=parent, name="Paul")
6731        catherine = Child.objects.create(parent=parent, name="Catherine")
6732        post = {
6733            "child_set-TOTAL_FORMS": "5",
6734            "child_set-INITIAL_FORMS": "2",
6735            "name": "Josh Stone",
6736            "child_set-0-name": "Paul",
6737            "child_set-0-id": paul.id,
6738            "child_set-1-name": "Catherine",
6739            "child_set-1-id": catherine.id,
6740        }
6741        self.client.post(
6742            reverse("admin:admin_views_parent_change", args=(parent.id,)), post
6743        )
6744
6745        children_names = list(
6746            Child.objects.order_by("name").values_list("name", flat=True)
6747        )
6748
6749        self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6750        self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6751
6752    def test_should_be_able_to_edit_related_objects_on_changelist_view(self):
6753        parent = Parent.objects.create(name="Josh Rock")
6754        Child.objects.create(parent=parent, name="Paul")
6755        Child.objects.create(parent=parent, name="Catherine")
6756        post = {
6757            "form-TOTAL_FORMS": "1",
6758            "form-INITIAL_FORMS": "1",
6759            "form-MAX_NUM_FORMS": "0",
6760            "form-0-id": parent.id,
6761            "form-0-name": "Josh Stone",
6762            "_save": "Save",
6763        }
6764
6765        self.client.post(reverse("admin:admin_views_parent_changelist"), post)
6766        children_names = list(
6767            Child.objects.order_by("name").values_list("name", flat=True)
6768        )
6769
6770        self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6771        self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6772
6773
6774@override_settings(ROOT_URLCONF="admin_views.urls")
6775class AdminViewLogoutTests(TestCase):
6776    @classmethod
6777    def setUpTestData(cls):
6778        cls.superuser = User.objects.create_superuser(
6779            username="super", password="secret", email="super@example.com"
6780        )
6781
6782    def test_logout(self):
6783        self.client.force_login(self.superuser)
6784        response = self.client.get(reverse("admin:logout"))
6785        self.assertEqual(response.status_code, 200)
6786        self.assertTemplateUsed(response, "registration/logged_out.html")
6787        self.assertEqual(response.request["PATH_INFO"], reverse("admin:logout"))
6788        self.assertFalse(response.context["has_permission"])
6789        self.assertNotContains(
6790            response, "user-tools"
6791        )  # user-tools div shouldn't visible.
6792
6793    def test_client_logout_url_can_be_used_to_login(self):
6794        response = self.client.get(reverse("admin:logout"))
6795        self.assertEqual(
6796            response.status_code, 302
6797        )  # we should be redirected to the login page.
6798
6799        # follow the redirect and test results.
6800        response = self.client.get(reverse("admin:logout"), follow=True)
6801        self.assertEqual(response.status_code, 200)
6802        self.assertTemplateUsed(response, "admin/login.html")
6803        self.assertEqual(response.request["PATH_INFO"], reverse("admin:login"))
6804        self.assertContains(
6805            response,
6806            '<input type="hidden" name="next" value="%s">' % reverse("admin:index"),
6807        )
6808
6809
6810@override_settings(ROOT_URLCONF="admin_views.urls")
6811class AdminUserMessageTest(TestCase):
6812    @classmethod
6813    def setUpTestData(cls):
6814        cls.superuser = User.objects.create_superuser(
6815            username="super", password="secret", email="super@example.com"
6816        )
6817
6818    def setUp(self):
6819        self.client.force_login(self.superuser)
6820
6821    def send_message(self, level):
6822        """
6823        Helper that sends a post to the dummy test methods and asserts that a
6824        message with the level has appeared in the response.
6825        """
6826        action_data = {
6827            ACTION_CHECKBOX_NAME: [1],
6828            "action": "message_%s" % level,
6829            "index": 0,
6830        }
6831
6832        response = self.client.post(
6833            reverse("admin:admin_views_usermessenger_changelist"),
6834            action_data,
6835            follow=True,
6836        )
6837        self.assertContains(
6838            response, '<li class="%s">Test %s</li>' % (level, level), html=True
6839        )
6840
6841    @override_settings(MESSAGE_LEVEL=10)  # Set to DEBUG for this request
6842    def test_message_debug(self):
6843        self.send_message("debug")
6844
6845    def test_message_info(self):
6846        self.send_message("info")
6847
6848    def test_message_success(self):
6849        self.send_message("success")
6850
6851    def test_message_warning(self):
6852        self.send_message("warning")
6853
6854    def test_message_error(self):
6855        self.send_message("error")
6856
6857    def test_message_extra_tags(self):
6858        action_data = {
6859            ACTION_CHECKBOX_NAME: [1],
6860            "action": "message_extra_tags",
6861            "index": 0,
6862        }
6863
6864        response = self.client.post(
6865            reverse("admin:admin_views_usermessenger_changelist"),
6866            action_data,
6867            follow=True,
6868        )
6869        self.assertContains(
6870            response, '<li class="extra_tag info">Test tags</li>', html=True
6871        )
6872
6873
6874@override_settings(ROOT_URLCONF="admin_views.urls")
6875class AdminKeepChangeListFiltersTests(TestCase):
6876    admin_site = site
6877
6878    @classmethod
6879    def setUpTestData(cls):
6880        cls.superuser = User.objects.create_superuser(
6881            username="super", password="secret", email="super@example.com"
6882        )
6883        cls.joepublicuser = User.objects.create_user(
6884            username="joepublic", password="secret"
6885        )
6886
6887    def setUp(self):
6888        self.client.force_login(self.superuser)
6889
6890    def assertURLEqual(self, url1, url2, msg_prefix=""):
6891        """
6892        Assert that two URLs are equal despite the ordering
6893        of their querystring. Refs #22360.
6894        """
6895        parsed_url1 = urlparse(url1)
6896        path1 = parsed_url1.path
6897        parsed_qs1 = dict(parse_qsl(parsed_url1.query))
6898
6899        parsed_url2 = urlparse(url2)
6900        path2 = parsed_url2.path
6901        parsed_qs2 = dict(parse_qsl(parsed_url2.query))
6902
6903        for parsed_qs in [parsed_qs1, parsed_qs2]:
6904            if "_changelist_filters" in parsed_qs:
6905                changelist_filters = parsed_qs["_changelist_filters"]
6906                parsed_filters = dict(parse_qsl(changelist_filters))
6907                parsed_qs["_changelist_filters"] = parsed_filters
6908
6909        self.assertEqual(path1, path2)
6910        self.assertEqual(parsed_qs1, parsed_qs2)
6911
6912    def test_assert_url_equal(self):
6913        # Test equality.
6914        change_user_url = reverse(
6915            "admin:auth_user_change", args=(self.joepublicuser.pk,)
6916        )
6917        self.assertURLEqual(
6918            "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6919                change_user_url
6920            ),
6921            "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6922                change_user_url
6923            ),
6924        )
6925
6926        # Test inequality.
6927        with self.assertRaises(AssertionError):
6928            self.assertURLEqual(
6929                "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6930                    change_user_url
6931                ),
6932                "http://testserver{}?_changelist_filters=is_staff__exact%3D1%26is_superuser__exact%3D1".format(
6933                    change_user_url
6934                ),
6935            )
6936
6937        # Ignore scheme and host.
6938        self.assertURLEqual(
6939            "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6940                change_user_url
6941            ),
6942            "{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6943                change_user_url
6944            ),
6945        )
6946
6947        # Ignore ordering of querystring.
6948        self.assertURLEqual(
6949            "{}?is_staff__exact=0&is_superuser__exact=0".format(
6950                reverse("admin:auth_user_changelist")
6951            ),
6952            "{}?is_superuser__exact=0&is_staff__exact=0".format(
6953                reverse("admin:auth_user_changelist")
6954            ),
6955        )
6956
6957        # Ignore ordering of _changelist_filters.
6958        self.assertURLEqual(
6959            "{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6960                change_user_url
6961            ),
6962            "{}?_changelist_filters=is_superuser__exact%3D0%26is_staff__exact%3D0".format(
6963                change_user_url
6964            ),
6965        )
6966
6967    def get_changelist_filters(self):
6968        return {
6969            "is_superuser__exact": 0,
6970            "is_staff__exact": 0,
6971        }
6972
6973    def get_changelist_filters_querystring(self):
6974        return urlencode(self.get_changelist_filters())
6975
6976    def get_preserved_filters_querystring(self):
6977        return urlencode(
6978            {"_changelist_filters": self.get_changelist_filters_querystring()}
6979        )
6980
6981    def get_sample_user_id(self):
6982        return self.joepublicuser.pk
6983
6984    def get_changelist_url(self):
6985        return "%s?%s" % (
6986            reverse("admin:auth_user_changelist", current_app=self.admin_site.name),
6987            self.get_changelist_filters_querystring(),
6988        )
6989
6990    def get_add_url(self):
6991        return "%s?%s" % (
6992            reverse("admin:auth_user_add", current_app=self.admin_site.name),
6993            self.get_preserved_filters_querystring(),
6994        )
6995
6996    def get_change_url(self, user_id=None):
6997        if user_id is None:
6998            user_id = self.get_sample_user_id()
6999        return "%s?%s" % (
7000            reverse(
7001                "admin:auth_user_change",
7002                args=(user_id,),
7003                current_app=self.admin_site.name,
7004            ),
7005            self.get_preserved_filters_querystring(),
7006        )
7007
7008    def get_history_url(self, user_id=None):
7009        if user_id is None:
7010            user_id = self.get_sample_user_id()
7011        return "%s?%s" % (
7012            reverse(
7013                "admin:auth_user_history",
7014                args=(user_id,),
7015                current_app=self.admin_site.name,
7016            ),
7017            self.get_preserved_filters_querystring(),
7018        )
7019
7020    def get_delete_url(self, user_id=None):
7021        if user_id is None:
7022            user_id = self.get_sample_user_id()
7023        return "%s?%s" % (
7024            reverse(
7025                "admin:auth_user_delete",
7026                args=(user_id,),
7027                current_app=self.admin_site.name,
7028            ),
7029            self.get_preserved_filters_querystring(),
7030        )
7031
7032    def test_changelist_view(self):
7033        response = self.client.get(self.get_changelist_url())
7034        self.assertEqual(response.status_code, 200)
7035
7036        # Check the `change_view` link has the correct querystring.
7037        detail_link = re.search(
7038            '<a href="(.*?)">{}</a>'.format(self.joepublicuser.username),
7039            response.content.decode(),
7040        )
7041        self.assertURLEqual(detail_link.group(1), self.get_change_url())
7042
7043    def test_change_view(self):
7044        # Get the `change_view`.
7045        response = self.client.get(self.get_change_url())
7046        self.assertEqual(response.status_code, 200)
7047
7048        # Check the form action.
7049        form_action = re.search(
7050            '<form action="(.*?)" method="post" id="user_form".*?>',
7051            response.content.decode(),
7052        )
7053        self.assertURLEqual(
7054            form_action.group(1), "?%s" % self.get_preserved_filters_querystring()
7055        )
7056
7057        # Check the history link.
7058        history_link = re.search(
7059            '<a href="(.*?)" class="historylink">History</a>', response.content.decode()
7060        )
7061        self.assertURLEqual(history_link.group(1), self.get_history_url())
7062
7063        # Check the delete link.
7064        delete_link = re.search(
7065            '<a href="(.*?)" class="deletelink">Delete</a>', response.content.decode()
7066        )
7067        self.assertURLEqual(delete_link.group(1), self.get_delete_url())
7068
7069        # Test redirect on "Save".
7070        post_data = {
7071            "username": "joepublic",
7072            "last_login_0": "2007-05-30",
7073            "last_login_1": "13:20:10",
7074            "date_joined_0": "2007-05-30",
7075            "date_joined_1": "13:20:10",
7076        }
7077
7078        post_data["_save"] = 1
7079        response = self.client.post(self.get_change_url(), data=post_data)
7080        self.assertRedirects(response, self.get_changelist_url())
7081        post_data.pop("_save")
7082
7083        # Test redirect on "Save and continue".
7084        post_data["_continue"] = 1
7085        response = self.client.post(self.get_change_url(), data=post_data)
7086        self.assertRedirects(response, self.get_change_url())
7087        post_data.pop("_continue")
7088
7089        # Test redirect on "Save and add new".
7090        post_data["_addanother"] = 1
7091        response = self.client.post(self.get_change_url(), data=post_data)
7092        self.assertRedirects(response, self.get_add_url())
7093        post_data.pop("_addanother")
7094
7095    def test_add_view(self):
7096        # Get the `add_view`.
7097        response = self.client.get(self.get_add_url())
7098        self.assertEqual(response.status_code, 200)
7099
7100        # Check the form action.
7101        form_action = re.search(
7102            '<form action="(.*?)" method="post" id="user_form".*?>',
7103            response.content.decode(),
7104        )
7105        self.assertURLEqual(
7106            form_action.group(1), "?%s" % self.get_preserved_filters_querystring()
7107        )
7108
7109        post_data = {
7110            "username": "dummy",
7111            "password1": "test",
7112            "password2": "test",
7113        }
7114
7115        # Test redirect on "Save".
7116        post_data["_save"] = 1
7117        response = self.client.post(self.get_add_url(), data=post_data)
7118        self.assertRedirects(
7119            response, self.get_change_url(User.objects.get(username="dummy").pk)
7120        )
7121        post_data.pop("_save")
7122
7123        # Test redirect on "Save and continue".
7124        post_data["username"] = "dummy2"
7125        post_data["_continue"] = 1
7126        response = self.client.post(self.get_add_url(), data=post_data)
7127        self.assertRedirects(
7128            response, self.get_change_url(User.objects.get(username="dummy2").pk)
7129        )
7130        post_data.pop("_continue")
7131
7132        # Test redirect on "Save and add new".
7133        post_data["username"] = "dummy3"
7134        post_data["_addanother"] = 1
7135        response = self.client.post(self.get_add_url(), data=post_data)
7136        self.assertRedirects(response, self.get_add_url())
7137        post_data.pop("_addanother")
7138
7139    def test_delete_view(self):
7140        # Test redirect on "Delete".
7141        response = self.client.post(self.get_delete_url(), {"post": "yes"})
7142        self.assertRedirects(response, self.get_changelist_url())
7143
7144    def test_url_prefix(self):
7145        context = {
7146            "preserved_filters": self.get_preserved_filters_querystring(),
7147            "opts": User._meta,
7148        }
7149        prefixes = ("", "/prefix/", "/後台/")
7150        for prefix in prefixes:
7151            with self.subTest(prefix=prefix), override_script_prefix(prefix):
7152                url = reverse(
7153                    "admin:auth_user_changelist", current_app=self.admin_site.name
7154                )
7155                self.assertURLEqual(
7156                    self.get_changelist_url(), add_preserved_filters(context, url),
7157                )
7158
7159
7160class NamespacedAdminKeepChangeListFiltersTests(AdminKeepChangeListFiltersTests):
7161    admin_site = site2
7162
7163
7164@override_settings(ROOT_URLCONF="admin_views.urls")
7165class TestLabelVisibility(TestCase):
7166    """ #11277 -Labels of hidden fields in admin were not hidden. """
7167
7168    @classmethod
7169    def setUpTestData(cls):
7170        cls.superuser = User.objects.create_superuser(
7171            username="super", password="secret", email="super@example.com"
7172        )
7173
7174    def setUp(self):
7175        self.client.force_login(self.superuser)
7176
7177    def test_all_fields_visible(self):
7178        response = self.client.get(reverse("admin:admin_views_emptymodelvisible_add"))
7179        self.assert_fieldline_visible(response)
7180        self.assert_field_visible(response, "first")
7181        self.assert_field_visible(response, "second")
7182
7183    def test_all_fields_hidden(self):
7184        response = self.client.get(reverse("admin:admin_views_emptymodelhidden_add"))
7185        self.assert_fieldline_hidden(response)
7186        self.assert_field_hidden(response, "first")
7187        self.assert_field_hidden(response, "second")
7188
7189    def test_mixin(self):
7190        response = self.client.get(reverse("admin:admin_views_emptymodelmixin_add"))
7191        self.assert_fieldline_visible(response)
7192        self.assert_field_hidden(response, "first")
7193        self.assert_field_visible(response, "second")
7194
7195    def assert_field_visible(self, response, field_name):
7196        self.assertContains(response, '<div class="fieldBox field-%s">' % field_name)
7197
7198    def assert_field_hidden(self, response, field_name):
7199        self.assertContains(
7200            response, '<div class="fieldBox field-%s hidden">' % field_name
7201        )
7202
7203    def assert_fieldline_visible(self, response):
7204        self.assertContains(response, '<div class="form-row field-first field-second">')
7205
7206    def assert_fieldline_hidden(self, response):
7207        self.assertContains(response, '<div class="form-row hidden')
7208
7209
7210@override_settings(ROOT_URLCONF="admin_views.urls")
7211class AdminViewOnSiteTests(TestCase):
7212    @classmethod
7213    def setUpTestData(cls):
7214        cls.superuser = User.objects.create_superuser(
7215            username="super", password="secret", email="super@example.com"
7216        )
7217
7218        cls.s1 = State.objects.create(name="New York")
7219        cls.s2 = State.objects.create(name="Illinois")
7220        cls.s3 = State.objects.create(name="California")
7221        cls.c1 = City.objects.create(state=cls.s1, name="New York")
7222        cls.c2 = City.objects.create(state=cls.s2, name="Chicago")
7223        cls.c3 = City.objects.create(state=cls.s3, name="San Francisco")
7224        cls.r1 = Restaurant.objects.create(city=cls.c1, name="Italian Pizza")
7225        cls.r2 = Restaurant.objects.create(city=cls.c1, name="Boulevard")
7226        cls.r3 = Restaurant.objects.create(city=cls.c2, name="Chinese Dinner")
7227        cls.r4 = Restaurant.objects.create(city=cls.c2, name="Angels")
7228        cls.r5 = Restaurant.objects.create(city=cls.c2, name="Take Away")
7229        cls.r6 = Restaurant.objects.create(city=cls.c3, name="The Unknown Restaurant")
7230        cls.w1 = Worker.objects.create(work_at=cls.r1, name="Mario", surname="Rossi")
7231        cls.w2 = Worker.objects.create(
7232            work_at=cls.r1, name="Antonio", surname="Bianchi"
7233        )
7234        cls.w3 = Worker.objects.create(work_at=cls.r1, name="John", surname="Doe")
7235
7236    def setUp(self):
7237        self.client.force_login(self.superuser)
7238
7239    def test_add_view_form_and_formsets_run_validation(self):
7240        """
7241        Issue #20522
7242        Verifying that if the parent form fails validation, the inlines also
7243        run validation even if validation is contingent on parent form data.
7244        Also, assertFormError() and assertFormsetError() is usable for admin
7245        forms and formsets.
7246        """
7247        # The form validation should fail because 'some_required_info' is
7248        # not included on the parent form, and the family_name of the parent
7249        # does not match that of the child
7250        post_data = {
7251            "family_name": "Test1",
7252            "dependentchild_set-TOTAL_FORMS": "1",
7253            "dependentchild_set-INITIAL_FORMS": "0",
7254            "dependentchild_set-MAX_NUM_FORMS": "1",
7255            "dependentchild_set-0-id": "",
7256            "dependentchild_set-0-parent": "",
7257            "dependentchild_set-0-family_name": "Test2",
7258        }
7259        response = self.client.post(
7260            reverse("admin:admin_views_parentwithdependentchildren_add"), post_data
7261        )
7262        self.assertFormError(
7263            response, "adminform", "some_required_info", ["This field is required."]
7264        )
7265        msg = "The form 'adminform' in context 0 does not contain the non-field error 'Error'"
7266        with self.assertRaisesMessage(AssertionError, msg):
7267            self.assertFormError(response, "adminform", None, ["Error"])
7268        self.assertFormsetError(
7269            response,
7270            "inline_admin_formset",
7271            0,
7272            None,
7273            [
7274                "Children must share a family name with their parents in this contrived test case"
7275            ],
7276        )
7277        msg = "The formset 'inline_admin_formset' in context 10 does not contain any non-form errors."
7278        with self.assertRaisesMessage(AssertionError, msg):
7279            self.assertFormsetError(
7280                response, "inline_admin_formset", None, None, ["Error"]
7281            )
7282
7283    def test_change_view_form_and_formsets_run_validation(self):
7284        """
7285        Issue #20522
7286        Verifying that if the parent form fails validation, the inlines also
7287        run validation even if validation is contingent on parent form data
7288        """
7289        pwdc = ParentWithDependentChildren.objects.create(
7290            some_required_info=6, family_name="Test1"
7291        )
7292        # The form validation should fail because 'some_required_info' is
7293        # not included on the parent form, and the family_name of the parent
7294        # does not match that of the child
7295        post_data = {
7296            "family_name": "Test2",
7297            "dependentchild_set-TOTAL_FORMS": "1",
7298            "dependentchild_set-INITIAL_FORMS": "0",
7299            "dependentchild_set-MAX_NUM_FORMS": "1",
7300            "dependentchild_set-0-id": "",
7301            "dependentchild_set-0-parent": str(pwdc.id),
7302            "dependentchild_set-0-family_name": "Test1",
7303        }
7304        response = self.client.post(
7305            reverse(
7306                "admin:admin_views_parentwithdependentchildren_change", args=(pwdc.id,)
7307            ),
7308            post_data,
7309        )
7310        self.assertFormError(
7311            response, "adminform", "some_required_info", ["This field is required."]
7312        )
7313        self.assertFormsetError(
7314            response,
7315            "inline_admin_formset",
7316            0,
7317            None,
7318            [
7319                "Children must share a family name with their parents in this contrived test case"
7320            ],
7321        )
7322
7323    def test_check(self):
7324        "The view_on_site value is either a boolean or a callable"
7325        try:
7326            admin = CityAdmin(City, AdminSite())
7327            CityAdmin.view_on_site = True
7328            self.assertEqual(admin.check(), [])
7329            CityAdmin.view_on_site = False
7330            self.assertEqual(admin.check(), [])
7331            CityAdmin.view_on_site = lambda obj: obj.get_absolute_url()
7332            self.assertEqual(admin.check(), [])
7333            CityAdmin.view_on_site = []
7334            self.assertEqual(
7335                admin.check(),
7336                [
7337                    Error(
7338                        "The value of 'view_on_site' must be a callable or a boolean value.",
7339                        obj=CityAdmin,
7340                        id="admin.E025",
7341                    ),
7342                ],
7343            )
7344        finally:
7345            # Restore the original values for the benefit of other tests.
7346            CityAdmin.view_on_site = True
7347
7348    def test_false(self):
7349        "The 'View on site' button is not displayed if view_on_site is False"
7350        response = self.client.get(
7351            reverse("admin:admin_views_restaurant_change", args=(self.r1.pk,))
7352        )
7353        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
7354        self.assertNotContains(
7355            response, reverse("admin:view_on_site", args=(content_type_pk, 1))
7356        )
7357
7358    def test_true(self):
7359        "The default behavior is followed if view_on_site is True"
7360        response = self.client.get(
7361            reverse("admin:admin_views_city_change", args=(self.c1.pk,))
7362        )
7363        content_type_pk = ContentType.objects.get_for_model(City).pk
7364        self.assertContains(
7365            response, reverse("admin:view_on_site", args=(content_type_pk, self.c1.pk))
7366        )
7367
7368    def test_callable(self):
7369        "The right link is displayed if view_on_site is a callable"
7370        response = self.client.get(
7371            reverse("admin:admin_views_worker_change", args=(self.w1.pk,))
7372        )
7373        self.assertContains(
7374            response, '"/worker/%s/%s/"' % (self.w1.surname, self.w1.name)
7375        )
7376
7377    def test_missing_get_absolute_url(self):
7378        "None is returned if model doesn't have get_absolute_url"
7379        model_admin = ModelAdmin(Worker, None)
7380        self.assertIsNone(model_admin.get_view_on_site_url(Worker()))
7381
7382
7383@override_settings(ROOT_URLCONF="admin_views.urls")
7384class InlineAdminViewOnSiteTest(TestCase):
7385    @classmethod
7386    def setUpTestData(cls):
7387        cls.superuser = User.objects.create_superuser(
7388            username="super", password="secret", email="super@example.com"
7389        )
7390
7391        cls.s1 = State.objects.create(name="New York")
7392        cls.s2 = State.objects.create(name="Illinois")
7393        cls.s3 = State.objects.create(name="California")
7394        cls.c1 = City.objects.create(state=cls.s1, name="New York")
7395        cls.c2 = City.objects.create(state=cls.s2, name="Chicago")
7396        cls.c3 = City.objects.create(state=cls.s3, name="San Francisco")
7397        cls.r1 = Restaurant.objects.create(city=cls.c1, name="Italian Pizza")
7398        cls.r2 = Restaurant.objects.create(city=cls.c1, name="Boulevard")
7399        cls.r3 = Restaurant.objects.create(city=cls.c2, name="Chinese Dinner")
7400        cls.r4 = Restaurant.objects.create(city=cls.c2, name="Angels")
7401        cls.r5 = Restaurant.objects.create(city=cls.c2, name="Take Away")
7402        cls.r6 = Restaurant.objects.create(city=cls.c3, name="The Unknown Restaurant")
7403        cls.w1 = Worker.objects.create(work_at=cls.r1, name="Mario", surname="Rossi")
7404        cls.w2 = Worker.objects.create(
7405            work_at=cls.r1, name="Antonio", surname="Bianchi"
7406        )
7407        cls.w3 = Worker.objects.create(work_at=cls.r1, name="John", surname="Doe")
7408
7409    def setUp(self):
7410        self.client.force_login(self.superuser)
7411
7412    def test_false(self):
7413        "The 'View on site' button is not displayed if view_on_site is False"
7414        response = self.client.get(
7415            reverse("admin:admin_views_state_change", args=(self.s1.pk,))
7416        )
7417        content_type_pk = ContentType.objects.get_for_model(City).pk
7418        self.assertNotContains(
7419            response, reverse("admin:view_on_site", args=(content_type_pk, self.c1.pk))
7420        )
7421
7422    def test_true(self):
7423        "The 'View on site' button is displayed if view_on_site is True"
7424        response = self.client.get(
7425            reverse("admin:admin_views_city_change", args=(self.c1.pk,))
7426        )
7427        content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
7428        self.assertContains(
7429            response, reverse("admin:view_on_site", args=(content_type_pk, self.r1.pk))
7430        )
7431
7432    def test_callable(self):
7433        "The right link is displayed if view_on_site is a callable"
7434        response = self.client.get(
7435            reverse("admin:admin_views_restaurant_change", args=(self.r1.pk,))
7436        )
7437        self.assertContains(
7438            response, '"/worker_inline/%s/%s/"' % (self.w1.surname, self.w1.name)
7439        )
7440
7441
7442@override_settings(ROOT_URLCONF="admin_views.urls")
7443class GetFormsetsWithInlinesArgumentTest(TestCase):
7444    """
7445    #23934 - When adding a new model instance in the admin, the 'obj' argument
7446    of get_formsets_with_inlines() should be None. When changing, it should be
7447    equal to the existing model instance.
7448    The GetFormsetsArgumentCheckingAdmin ModelAdmin throws an exception
7449    if obj is not None during add_view or obj is None during change_view.
7450    """
7451
7452    @classmethod
7453    def setUpTestData(cls):
7454        cls.superuser = User.objects.create_superuser(
7455            username="super", password="secret", email="super@example.com"
7456        )
7457
7458    def setUp(self):
7459        self.client.force_login(self.superuser)
7460
7461    def test_explicitly_provided_pk(self):
7462        post_data = {"name": "1"}
7463        response = self.client.post(
7464            reverse("admin:admin_views_explicitlyprovidedpk_add"), post_data
7465        )
7466        self.assertEqual(response.status_code, 302)
7467
7468        post_data = {"name": "2"}
7469        response = self.client.post(
7470            reverse("admin:admin_views_explicitlyprovidedpk_change", args=(1,)),
7471            post_data,
7472        )
7473        self.assertEqual(response.status_code, 302)
7474
7475    def test_implicitly_generated_pk(self):
7476        post_data = {"name": "1"}
7477        response = self.client.post(
7478            reverse("admin:admin_views_implicitlygeneratedpk_add"), post_data
7479        )
7480        self.assertEqual(response.status_code, 302)
7481
7482        post_data = {"name": "2"}
7483        response = self.client.post(
7484            reverse("admin:admin_views_implicitlygeneratedpk_change", args=(1,)),
7485            post_data,
7486        )
7487        self.assertEqual(response.status_code, 302)