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