django.contrib.admin admin_order_field ¶
See also
Contents
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 )
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": "<p>Middle content</p>",
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": "<p>Oldest content</p>",
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": "<p>Newest content</p>",
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 = '› <a href="%s">Articles</a>' % reverse(
2135 "admin:admin_views_article_changelist"
2136 )
2137 self.assertNotContains(
2138 addpage,
2139 change_list_link,
2140 msg_prefix="User restricted to add permission is given link to change list view in breadcrumbs.",
2141 )
2142 post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2143 self.assertRedirects(post, self.index_url)
2144 self.assertEqual(Article.objects.count(), 4)
2145 self.assertEqual(len(mail.outbox), 2)
2146 self.assertEqual(mail.outbox[0].subject, "Greetings from a created object")
2147 self.client.get(reverse("admin:logout"))
2148
2149 # The addition was logged correctly
2150 addition_log = LogEntry.objects.all()[0]
2151 new_article = Article.objects.last()
2152 article_ct = ContentType.objects.get_for_model(Article)
2153 self.assertEqual(addition_log.user_id, self.adduser.pk)
2154 self.assertEqual(addition_log.content_type_id, article_ct.pk)
2155 self.assertEqual(addition_log.object_id, str(new_article.pk))
2156 self.assertEqual(addition_log.object_repr, "Døm ikke")
2157 self.assertEqual(addition_log.action_flag, ADDITION)
2158 self.assertEqual(addition_log.get_change_message(), "Added.")
2159
2160 # Super can add too, but is redirected to the change list view
2161 self.client.force_login(self.superuser)
2162 addpage = self.client.get(reverse("admin:admin_views_article_add"))
2163 self.assertContains(
2164 addpage,
2165 change_list_link,
2166 msg_prefix="Unrestricted user is not given link to change list view in breadcrumbs.",
2167 )
2168 post = self.client.post(reverse("admin:admin_views_article_add"), add_dict)
2169 self.assertRedirects(post, reverse("admin:admin_views_article_changelist"))
2170 self.assertEqual(Article.objects.count(), 5)
2171 self.client.get(reverse("admin:logout"))
2172
2173 # 8509 - if a normal user is already logged in, it is possible
2174 # to change user into the superuser without error
2175 self.client.force_login(self.joepublicuser)
2176 # Check and make sure that if user expires, data still persists
2177 self.client.force_login(self.superuser)
2178 # make sure the view removes test cookie
2179 self.assertIs(self.client.session.test_cookie_worked(), False)
2180
2181 @mock.patch("django.contrib.admin.options.InlineModelAdmin.has_change_permission")
2182 def test_add_view_with_view_only_inlines(self, has_change_permission):
2183 """User with add permission to a section but view-only for inlines."""
2184 self.viewuser.user_permissions.add(
2185 get_perm(Section, get_permission_codename("add", Section._meta))
2186 )
2187 self.client.force_login(self.viewuser)
2188 # Valid POST creates a new section.
2189 data = {
2190 "name": "New obj",
2191 "article_set-TOTAL_FORMS": 0,
2192 "article_set-INITIAL_FORMS": 0,
2193 }
2194 response = self.client.post(reverse("admin:admin_views_section_add"), data)
2195 self.assertRedirects(response, reverse("admin:index"))
2196 self.assertEqual(Section.objects.latest("id").name, data["name"])
2197 # InlineModelAdmin.has_change_permission()'s obj argument is always
2198 # None during object add.
2199 self.assertEqual(
2200 [obj for (request, obj), _ in has_change_permission.call_args_list],
2201 [None, None],
2202 )
2203
2204 def test_change_view(self):
2205 """Change view should restrict access and allow users to edit items."""
2206 change_dict = {
2207 "title": "Ikke fordømt",
2208 "content": "<p>edited article</p>",
2209 "date_0": "2008-03-18",
2210 "date_1": "10:54:39",
2211 "section": self.s1.pk,
2212 }
2213 article_change_url = reverse(
2214 "admin:admin_views_article_change", args=(self.a1.pk,)
2215 )
2216 article_changelist_url = reverse("admin:admin_views_article_changelist")
2217
2218 # add user should not be able to view the list of article or change any of them
2219 self.client.force_login(self.adduser)
2220 response = self.client.get(article_changelist_url)
2221 self.assertEqual(response.status_code, 403)
2222 response = self.client.get(article_change_url)
2223 self.assertEqual(response.status_code, 403)
2224 post = self.client.post(article_change_url, change_dict)
2225 self.assertEqual(post.status_code, 403)
2226 self.client.get(reverse("admin:logout"))
2227
2228 # view user can view articles but not make changes.
2229 self.client.force_login(self.viewuser)
2230 response = self.client.get(article_changelist_url)
2231 self.assertEqual(response.status_code, 200)
2232 self.assertEqual(response.context["title"], "Select article to view")
2233 response = self.client.get(article_change_url)
2234 self.assertEqual(response.status_code, 200)
2235 self.assertEqual(response.context["title"], "View article")
2236 self.assertContains(response, "<label>Extra form field:</label>")
2237 self.assertContains(
2238 response,
2239 '<a href="/test_admin/admin/admin_views/article/" class="closelink">Close</a>',
2240 )
2241 post = self.client.post(article_change_url, change_dict)
2242 self.assertEqual(post.status_code, 403)
2243 self.assertEqual(
2244 Article.objects.get(pk=self.a1.pk).content, "<p>Middle content</p>"
2245 )
2246 self.client.get(reverse("admin:logout"))
2247
2248 # change user can view all items and edit them
2249 self.client.force_login(self.changeuser)
2250 response = self.client.get(article_changelist_url)
2251 self.assertEqual(response.status_code, 200)
2252 self.assertEqual(response.context["title"], "Select article to change")
2253 response = self.client.get(article_change_url)
2254 self.assertEqual(response.status_code, 200)
2255 self.assertEqual(response.context["title"], "Change article")
2256 post = self.client.post(article_change_url, change_dict)
2257 self.assertRedirects(post, article_changelist_url)
2258 self.assertEqual(
2259 Article.objects.get(pk=self.a1.pk).content, "<p>edited article</p>"
2260 )
2261
2262 # one error in form should produce singular error message, multiple errors plural
2263 change_dict["title"] = ""
2264 post = self.client.post(article_change_url, change_dict)
2265 self.assertContains(
2266 post,
2267 "Please correct the error below.",
2268 msg_prefix="Singular error message not found in response to post with one error",
2269 )
2270
2271 change_dict["content"] = ""
2272 post = self.client.post(article_change_url, change_dict)
2273 self.assertContains(
2274 post,
2275 "Please correct the errors below.",
2276 msg_prefix="Plural error message not found in response to post with multiple errors",
2277 )
2278 self.client.get(reverse("admin:logout"))
2279
2280 # Test redirection when using row-level change permissions. Refs #11513.
2281 r1 = RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
2282 r2 = RowLevelChangePermissionModel.objects.create(id=2, name="even id")
2283 r3 = RowLevelChangePermissionModel.objects.create(id=3, name="odd id mult 3")
2284 r6 = RowLevelChangePermissionModel.objects.create(id=6, name="even id mult 3")
2285 change_url_1 = reverse(
2286 "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r1.pk,)
2287 )
2288 change_url_2 = reverse(
2289 "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r2.pk,)
2290 )
2291 change_url_3 = reverse(
2292 "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r3.pk,)
2293 )
2294 change_url_6 = reverse(
2295 "admin:admin_views_rowlevelchangepermissionmodel_change", args=(r6.pk,)
2296 )
2297 logins = [
2298 self.superuser,
2299 self.viewuser,
2300 self.adduser,
2301 self.changeuser,
2302 self.deleteuser,
2303 ]
2304 for login_user in logins:
2305 with self.subTest(login_user.username):
2306 self.client.force_login(login_user)
2307 response = self.client.get(change_url_1)
2308 self.assertEqual(response.status_code, 403)
2309 response = self.client.post(change_url_1, {"name": "changed"})
2310 self.assertEqual(
2311 RowLevelChangePermissionModel.objects.get(id=