django.contrib.admin admin_order_field ¶
See also
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=1).name, "odd id"
2312 )
2313 self.assertEqual(response.status_code, 403)
2314 response = self.client.get(change_url_2)
2315 self.assertEqual(response.status_code, 200)
2316 response = self.client.post(change_url_2, {"name": "changed"})
2317 self.assertEqual(
2318 RowLevelChangePermissionModel.objects.get(id=2).name, "changed"
2319 )
2320 self.assertRedirects(response, self.index_url)
2321 response = self.client.get(change_url_3)
2322 self.assertEqual(response.status_code, 200)
2323 response = self.client.post(change_url_3, {"name": "changed"})
2324 self.assertEqual(response.status_code, 403)
2325 self.assertEqual(
2326 RowLevelChangePermissionModel.objects.get(id=3).name,
2327 "odd id mult 3",
2328 )
2329 response = self.client.get(change_url_6)
2330 self.assertEqual(response.status_code, 200)
2331 response = self.client.post(change_url_6, {"name": "changed"})
2332 self.assertEqual(
2333 RowLevelChangePermissionModel.objects.get(id=6).name, "changed"
2334 )
2335 self.assertRedirects(response, self.index_url)
2336
2337 self.client.get(reverse("admin:logout"))
2338
2339 for login_user in [self.joepublicuser, self.nostaffuser]:
2340 with self.subTest(login_user.username):
2341 self.client.force_login(login_user)
2342 response = self.client.get(change_url_1, follow=True)
2343 self.assertContains(response, "login-form")
2344 response = self.client.post(
2345 change_url_1, {"name": "changed"}, follow=True
2346 )
2347 self.assertEqual(
2348 RowLevelChangePermissionModel.objects.get(id=1).name, "odd id"
2349 )
2350 self.assertContains(response, "login-form")
2351 response = self.client.get(change_url_2, follow=True)
2352 self.assertContains(response, "login-form")
2353 response = self.client.post(
2354 change_url_2, {"name": "changed again"}, follow=True
2355 )
2356 self.assertEqual(
2357 RowLevelChangePermissionModel.objects.get(id=2).name, "changed"
2358 )
2359 self.assertContains(response, "login-form")
2360 self.client.get(reverse("admin:logout"))
2361
2362 def test_change_view_without_object_change_permission(self):
2363 """
2364 The object should be read-only if the user has permission to view it
2365 and change objects of that type but not to change the current object.
2366 """
2367 change_url = reverse("admin9:admin_views_article_change", args=(self.a1.pk,))
2368 self.client.force_login(self.viewuser)
2369 response = self.client.get(change_url)
2370 self.assertEqual(response.status_code, 200)
2371 self.assertEqual(response.context["title"], "View article")
2372 self.assertContains(
2373 response,
2374 '<a href="/test_admin/admin9/admin_views/article/" class="closelink">Close</a>',
2375 )
2376
2377 def test_change_view_save_as_new(self):
2378 """
2379 'Save as new' should raise PermissionDenied for users without the 'add'
2380 permission.
2381 """
2382 change_dict_save_as_new = {
2383 "_saveasnew": "Save as new",
2384 "title": "Ikke fordømt",
2385 "content": "<p>edited article</p>",
2386 "date_0": "2008-03-18",
2387 "date_1": "10:54:39",
2388 "section": self.s1.pk,
2389 }
2390 article_change_url = reverse(
2391 "admin:admin_views_article_change", args=(self.a1.pk,)
2392 )
2393
2394 # Add user can perform "Save as new".
2395 article_count = Article.objects.count()
2396 self.client.force_login(self.adduser)
2397 post = self.client.post(article_change_url, change_dict_save_as_new)
2398 self.assertRedirects(post, self.index_url)
2399 self.assertEqual(Article.objects.count(), article_count + 1)
2400 self.client.logout()
2401
2402 # Change user cannot perform "Save as new" (no 'add' permission).
2403 article_count = Article.objects.count()
2404 self.client.force_login(self.changeuser)
2405 post = self.client.post(article_change_url, change_dict_save_as_new)
2406 self.assertEqual(post.status_code, 403)
2407 self.assertEqual(Article.objects.count(), article_count)
2408
2409 # User with both add and change permissions should be redirected to the
2410 # change page for the newly created object.
2411 article_count = Article.objects.count()
2412 self.client.force_login(self.superuser)
2413 post = self.client.post(article_change_url, change_dict_save_as_new)
2414 self.assertEqual(Article.objects.count(), article_count + 1)
2415 new_article = Article.objects.latest("id")
2416 self.assertRedirects(
2417 post, reverse("admin:admin_views_article_change", args=(new_article.pk,))
2418 )
2419
2420 def test_change_view_with_view_only_inlines(self):
2421 """
2422 User with change permission to a section but view-only for inlines.
2423 """
2424 self.viewuser.user_permissions.add(
2425 get_perm(Section, get_permission_codename("change", Section._meta))
2426 )
2427 self.client.force_login(self.viewuser)
2428 # GET shows inlines.
2429 response = self.client.get(
2430 reverse("admin:admin_views_section_change", args=(self.s1.pk,))
2431 )
2432 self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2433 formset = response.context["inline_admin_formsets"][0]
2434 self.assertEqual(len(formset.forms), 3)
2435 # Valid POST changes the name.
2436 data = {
2437 "name": "Can edit name with view-only inlines",
2438 "article_set-TOTAL_FORMS": 3,
2439 "article_set-INITIAL_FORMS": 3,
2440 }
2441 response = self.client.post(
2442 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2443 )
2444 self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2445 self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data["name"])
2446 # Invalid POST reshows inlines.
2447 del data["name"]
2448 response = self.client.post(
2449 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2450 )
2451 self.assertEqual(response.status_code, 200)
2452 self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2453 formset = response.context["inline_admin_formsets"][0]
2454 self.assertEqual(len(formset.forms), 3)
2455
2456 def test_change_view_with_view_and_add_inlines(self):
2457 """User has view and add permissions on the inline model."""
2458 self.viewuser.user_permissions.add(
2459 get_perm(Section, get_permission_codename("change", Section._meta))
2460 )
2461 self.viewuser.user_permissions.add(
2462 get_perm(Article, get_permission_codename("add", Article._meta))
2463 )
2464 self.client.force_login(self.viewuser)
2465 # GET shows inlines.
2466 response = self.client.get(
2467 reverse("admin:admin_views_section_change", args=(self.s1.pk,))
2468 )
2469 self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2470 formset = response.context["inline_admin_formsets"][0]
2471 self.assertEqual(len(formset.forms), 6)
2472 # Valid POST creates a new article.
2473 data = {
2474 "name": "Can edit name with view-only inlines",
2475 "article_set-TOTAL_FORMS": 6,
2476 "article_set-INITIAL_FORMS": 3,
2477 "article_set-3-id": [""],
2478 "article_set-3-title": ["A title"],
2479 "article_set-3-content": ["Added content"],
2480 "article_set-3-date_0": ["2008-3-18"],
2481 "article_set-3-date_1": ["11:54:58"],
2482 "article_set-3-section": [str(self.s1.pk)],
2483 }
2484 response = self.client.post(
2485 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2486 )
2487 self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2488 self.assertEqual(Section.objects.get(pk=self.s1.pk).name, data["name"])
2489 self.assertEqual(Article.objects.count(), 4)
2490 # Invalid POST reshows inlines.
2491 del data["name"]
2492 response = self.client.post(
2493 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2494 )
2495 self.assertEqual(response.status_code, 200)
2496 self.assertEqual(len(response.context["inline_admin_formsets"]), 1)
2497 formset = response.context["inline_admin_formsets"][0]
2498 self.assertEqual(len(formset.forms), 6)
2499
2500 def test_change_view_with_view_and_delete_inlines(self):
2501 """User has view and delete permissions on the inline model."""
2502 self.viewuser.user_permissions.add(
2503 get_perm(Section, get_permission_codename("change", Section._meta))
2504 )
2505 self.client.force_login(self.viewuser)
2506 data = {
2507 "name": "Name is required.",
2508 "article_set-TOTAL_FORMS": 6,
2509 "article_set-INITIAL_FORMS": 3,
2510 "article_set-0-id": [str(self.a1.pk)],
2511 "article_set-0-DELETE": ["on"],
2512 }
2513 # Inline POST details are ignored without delete permission.
2514 response = self.client.post(
2515 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2516 )
2517 self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2518 self.assertEqual(Article.objects.count(), 3)
2519 # Deletion successful when delete permission is added.
2520 self.viewuser.user_permissions.add(
2521 get_perm(Article, get_permission_codename("delete", Article._meta))
2522 )
2523 data = {
2524 "name": "Name is required.",
2525 "article_set-TOTAL_FORMS": 6,
2526 "article_set-INITIAL_FORMS": 3,
2527 "article_set-0-id": [str(self.a1.pk)],
2528 "article_set-0-DELETE": ["on"],
2529 }
2530 response = self.client.post(
2531 reverse("admin:admin_views_section_change", args=(self.s1.pk,)), data
2532 )
2533 self.assertRedirects(response, reverse("admin:admin_views_section_changelist"))
2534 self.assertEqual(Article.objects.count(), 2)
2535
2536 def test_delete_view(self):
2537 """Delete view should restrict access and actually delete items."""
2538 delete_dict = {"post": "yes"}
2539 delete_url = reverse("admin:admin_views_article_delete", args=(self.a1.pk,))
2540
2541 # add user should not be able to delete articles
2542 self.client.force_login(self.adduser)
2543 response = self.client.get(delete_url)
2544 self.assertEqual(response.status_code, 403)
2545 post = self.client.post(delete_url, delete_dict)
2546 self.assertEqual(post.status_code, 403)
2547 self.assertEqual(Article.objects.count(), 3)
2548 self.client.logout()
2549
2550 # view user should not be able to delete articles
2551 self.client.force_login(self.viewuser)
2552 response = self.client.get(delete_url)
2553 self.assertEqual(response.status_code, 403)
2554 post = self.client.post(delete_url, delete_dict)
2555 self.assertEqual(post.status_code, 403)
2556 self.assertEqual(Article.objects.count(), 3)
2557 self.client.logout()
2558
2559 # Delete user can delete
2560 self.client.force_login(self.deleteuser)
2561 response = self.client.get(
2562 reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
2563 )
2564 self.assertContains(response, "<h2>Summary</h2>")
2565 self.assertContains(response, "<li>Articles: 3</li>")
2566 # test response contains link to related Article
2567 self.assertContains(response, "admin_views/article/%s/" % self.a1.pk)
2568
2569 response = self.client.get(delete_url)
2570 self.assertContains(response, "admin_views/article/%s/" % self.a1.pk)
2571 self.assertContains(response, "<h2>Summary</h2>")
2572 self.assertContains(response, "<li>Articles: 1</li>")
2573 self.assertEqual(response.status_code, 200)
2574 post = self.client.post(delete_url, delete_dict)
2575 self.assertRedirects(post, self.index_url)
2576 self.assertEqual(Article.objects.count(), 2)
2577 self.assertEqual(len(mail.outbox), 1)
2578 self.assertEqual(mail.outbox[0].subject, "Greetings from a deleted object")
2579 article_ct = ContentType.objects.get_for_model(Article)
2580 logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION)
2581 self.assertEqual(logged.object_id, str(self.a1.pk))
2582
2583 def test_delete_view_with_no_default_permissions(self):
2584 """
2585 The delete view allows users to delete collected objects without a
2586 'delete' permission (ReadOnlyPizza.Meta.default_permissions is empty).
2587 """
2588 pizza = ReadOnlyPizza.objects.create(name="Double Cheese")
2589 delete_url = reverse("admin:admin_views_readonlypizza_delete", args=(pizza.pk,))
2590 self.client.force_login(self.adduser)
2591 response = self.client.get(delete_url)
2592 self.assertContains(response, "admin_views/readonlypizza/%s/" % pizza.pk)
2593 self.assertContains(response, "<h2>Summary</h2>")
2594 self.assertContains(response, "<li>Read only pizzas: 1</li>")
2595 self.assertEqual(response.status_code, 200)
2596 post = self.client.post(delete_url, {"post": "yes"})
2597 self.assertRedirects(
2598 post, reverse("admin:admin_views_readonlypizza_changelist")
2599 )
2600 self.assertEqual(ReadOnlyPizza.objects.count(), 0)
2601
2602 def test_delete_view_nonexistent_obj(self):
2603 self.client.force_login(self.deleteuser)
2604 url = reverse("admin:admin_views_article_delete", args=("nonexistent",))
2605 response = self.client.get(url, follow=True)
2606 self.assertRedirects(response, reverse("admin:index"))
2607 self.assertEqual(
2608 [m.message for m in response.context["messages"]],
2609 ["article with ID “nonexistent” doesn’t exist. Perhaps it was deleted?"],
2610 )
2611
2612 def test_history_view(self):
2613 """History view should restrict access."""
2614 # add user should not be able to view the list of article or change any of them
2615 self.client.force_login(self.adduser)
2616 response = self.client.get(
2617 reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2618 )
2619 self.assertEqual(response.status_code, 403)
2620 self.client.get(reverse("admin:logout"))
2621
2622 # view user can view all items
2623 self.client.force_login(self.viewuser)
2624 response = self.client.get(
2625 reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2626 )
2627 self.assertEqual(response.status_code, 200)
2628 self.client.get(reverse("admin:logout"))
2629
2630 # change user can view all items and edit them
2631 self.client.force_login(self.changeuser)
2632 response = self.client.get(
2633 reverse("admin:admin_views_article_history", args=(self.a1.pk,))
2634 )
2635 self.assertEqual(response.status_code, 200)
2636
2637 # Test redirection when using row-level change permissions. Refs #11513.
2638 rl1 = RowLevelChangePermissionModel.objects.create(name="odd id")
2639 rl2 = RowLevelChangePermissionModel.objects.create(name="even id")
2640 logins = [
2641 self.superuser,
2642 self.viewuser,
2643 self.adduser,
2644 self.changeuser,
2645 self.deleteuser,
2646 ]
2647 for login_user in logins:
2648 with self.subTest(login_user.username):
2649 self.client.force_login(login_user)
2650 url = reverse(
2651 "admin:admin_views_rowlevelchangepermissionmodel_history",
2652 args=(rl1.pk,),
2653 )
2654 response = self.client.get(url)
2655 self.assertEqual(response.status_code, 403)
2656
2657 url = reverse(
2658 "admin:admin_views_rowlevelchangepermissionmodel_history",
2659 args=(rl2.pk,),
2660 )
2661 response = self.client.get(url)
2662 self.assertEqual(response.status_code, 200)
2663
2664 self.client.get(reverse("admin:logout"))
2665
2666 for login_user in [self.joepublicuser, self.nostaffuser]:
2667 with self.subTest(login_user.username):
2668 self.client.force_login(login_user)
2669 url = reverse(
2670 "admin:admin_views_rowlevelchangepermissionmodel_history",
2671 args=(rl1.pk,),
2672 )
2673 response = self.client.get(url, follow=True)
2674 self.assertContains(response, "login-form")
2675 url = reverse(
2676 "admin:admin_views_rowlevelchangepermissionmodel_history",
2677 args=(rl2.pk,),
2678 )
2679 response = self.client.get(url, follow=True)
2680 self.assertContains(response, "login-form")
2681
2682 self.client.get(reverse("admin:logout"))
2683
2684 def test_history_view_bad_url(self):
2685 self.client.force_login(self.changeuser)
2686 response = self.client.get(
2687 reverse("admin:admin_views_article_history", args=("foo",)), follow=True
2688 )
2689 self.assertRedirects(response, reverse("admin:index"))
2690 self.assertEqual(
2691 [m.message for m in response.context["messages"]],
2692 ["article with ID “foo” doesn’t exist. Perhaps it was deleted?"],
2693 )
2694
2695 def test_conditionally_show_add_section_link(self):
2696 """
2697 The foreign key widget should only show the "add related" button if the
2698 user has permission to add that related item.
2699 """
2700 self.client.force_login(self.adduser)
2701 # The user can't add sections yet, so they shouldn't see the "add section" link.
2702 url = reverse("admin:admin_views_article_add")
2703 add_link_text = "add_id_section"
2704 response = self.client.get(url)
2705 self.assertNotContains(response, add_link_text)
2706 # Allow the user to add sections too. Now they can see the "add section" link.
2707 user = User.objects.get(username="adduser")
2708 perm = get_perm(Section, get_permission_codename("add", Section._meta))
2709 user.user_permissions.add(perm)
2710 response = self.client.get(url)
2711 self.assertContains(response, add_link_text)
2712
2713 def test_conditionally_show_change_section_link(self):
2714 """
2715 The foreign key widget should only show the "change related" button if
2716 the user has permission to change that related item.
2717 """
2718
2719 def get_change_related(response):
2720 return (
2721 response.context["adminform"]
2722 .form.fields["section"]
2723 .widget.can_change_related
2724 )
2725
2726 self.client.force_login(self.adduser)
2727 # The user can't change sections yet, so they shouldn't see the "change section" link.
2728 url = reverse("admin:admin_views_article_add")
2729 change_link_text = "change_id_section"
2730 response = self.client.get(url)
2731 self.assertFalse(get_change_related(response))
2732 self.assertNotContains(response, change_link_text)
2733 # Allow the user to change sections too. Now they can see the "change section" link.
2734 user = User.objects.get(username="adduser")
2735 perm = get_perm(Section, get_permission_codename("change", Section._meta))
2736 user.user_permissions.add(perm)
2737 response = self.client.get(url)
2738 self.assertTrue(get_change_related(response))
2739 self.assertContains(response, change_link_text)
2740
2741 def test_conditionally_show_delete_section_link(self):
2742 """
2743 The foreign key widget should only show the "delete related" button if
2744 the user has permission to delete that related item.
2745 """
2746
2747 def get_delete_related(response):
2748 return (
2749 response.context["adminform"]
2750 .form.fields["sub_section"]
2751 .widget.can_delete_related
2752 )
2753
2754 self.client.force_login(self.adduser)
2755 # The user can't delete sections yet, so they shouldn't see the "delete section" link.
2756 url = reverse("admin:admin_views_article_add")
2757 delete_link_text = "delete_id_sub_section"
2758 response = self.client.get(url)
2759 self.assertFalse(get_delete_related(response))
2760 self.assertNotContains(response, delete_link_text)
2761 # Allow the user to delete sections too. Now they can see the "delete section" link.
2762 user = User.objects.get(username="adduser")
2763 perm = get_perm(Section, get_permission_codename("delete", Section._meta))
2764 user.user_permissions.add(perm)
2765 response = self.client.get(url)
2766 self.assertTrue(get_delete_related(response))
2767 self.assertContains(response, delete_link_text)
2768
2769 def test_disabled_permissions_when_logged_in(self):
2770 self.client.force_login(self.superuser)
2771 superuser = User.objects.get(username="super")
2772 superuser.is_active = False
2773 superuser.save()
2774
2775 response = self.client.get(self.index_url, follow=True)
2776 self.assertContains(response, 'id="login-form"')
2777 self.assertNotContains(response, "Log out")
2778
2779 response = self.client.get(reverse("secure_view"), follow=True)
2780 self.assertContains(response, 'id="login-form"')
2781
2782 def test_disabled_staff_permissions_when_logged_in(self):
2783 self.client.force_login(self.superuser)
2784 superuser = User.objects.get(username="super")
2785 superuser.is_staff = False
2786 superuser.save()
2787
2788 response = self.client.get(self.index_url, follow=True)
2789 self.assertContains(response, 'id="login-form"')
2790 self.assertNotContains(response, "Log out")
2791
2792 response = self.client.get(reverse("secure_view"), follow=True)
2793 self.assertContains(response, 'id="login-form"')
2794
2795 def test_app_list_permissions(self):
2796 """
2797 If a user has no module perms, the app list returns a 404.
2798 """
2799 opts = Article._meta
2800 change_user = User.objects.get(username="changeuser")
2801 permission = get_perm(Article, get_permission_codename("change", opts))
2802
2803 self.client.force_login(self.changeuser)
2804
2805 # the user has no module permissions
2806 change_user.user_permissions.remove(permission)
2807 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
2808 self.assertEqual(response.status_code, 404)
2809
2810 # the user now has module permissions
2811 change_user.user_permissions.add(permission)
2812 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
2813 self.assertEqual(response.status_code, 200)
2814
2815 def test_shortcut_view_only_available_to_staff(self):
2816 """
2817 Only admin users should be able to use the admin shortcut view.
2818 """
2819 model_ctype = ContentType.objects.get_for_model(ModelWithStringPrimaryKey)
2820 obj = ModelWithStringPrimaryKey.objects.create(string_pk="foo")
2821 shortcut_url = reverse("admin:view_on_site", args=(model_ctype.pk, obj.pk))
2822
2823 # Not logged in: we should see the login page.
2824 response = self.client.get(shortcut_url, follow=True)
2825 self.assertTemplateUsed(response, "admin/login.html")
2826
2827 # Logged in? Redirect.
2828 self.client.force_login(self.superuser)
2829 response = self.client.get(shortcut_url, follow=False)
2830 # Can't use self.assertRedirects() because User.get_absolute_url() is silly.
2831 self.assertEqual(response.status_code, 302)
2832 # Domain may depend on contrib.sites tests also run
2833 self.assertRegex(response.url, "http://(testserver|example.com)/dummy/foo/")
2834
2835 def test_has_module_permission(self):
2836 """
2837 has_module_permission() returns True for all users who
2838 have any permission for that module (add, change, or delete), so that
2839 the module is displayed on the admin index page.
2840 """
2841 self.client.force_login(self.superuser)
2842 response = self.client.get(self.index_url)
2843 self.assertContains(response, "admin_views")
2844 self.assertContains(response, "Articles")
2845 self.client.logout()
2846
2847 self.client.force_login(self.viewuser)
2848 response = self.client.get(self.index_url)
2849 self.assertContains(response, "admin_views")
2850 self.assertContains(response, "Articles")
2851 self.client.logout()
2852
2853 self.client.force_login(self.adduser)
2854 response = self.client.get(self.index_url)
2855 self.assertContains(response, "admin_views")
2856 self.assertContains(response, "Articles")
2857 self.client.logout()
2858
2859 self.client.force_login(self.changeuser)
2860 response = self.client.get(self.index_url)
2861 self.assertContains(response, "admin_views")
2862 self.assertContains(response, "Articles")
2863 self.client.logout()
2864
2865 self.client.force_login(self.deleteuser)
2866 response = self.client.get(self.index_url)
2867 self.assertContains(response, "admin_views")
2868 self.assertContains(response, "Articles")
2869
2870 def test_overriding_has_module_permission(self):
2871 """
2872 If has_module_permission() always returns False, the module shouldn't
2873 be displayed on the admin index page for any users.
2874 """
2875 articles = Article._meta.verbose_name_plural.title()
2876 sections = Section._meta.verbose_name_plural.title()
2877 index_url = reverse("admin7:index")
2878
2879 self.client.force_login(self.superuser)
2880 response = self.client.get(index_url)
2881 self.assertContains(response, sections)
2882 self.assertNotContains(response, articles)
2883 self.client.logout()
2884
2885 self.client.force_login(self.viewuser)
2886 response = self.client.get(index_url)
2887 self.assertNotContains(response, "admin_views")
2888 self.assertNotContains(response, articles)
2889 self.client.logout()
2890
2891 self.client.force_login(self.adduser)
2892 response = self.client.get(index_url)
2893 self.assertNotContains(response, "admin_views")
2894 self.assertNotContains(response, articles)
2895 self.client.logout()
2896
2897 self.client.force_login(self.changeuser)
2898 response = self.client.get(index_url)
2899 self.assertNotContains(response, "admin_views")
2900 self.assertNotContains(response, articles)
2901 self.client.logout()
2902
2903 self.client.force_login(self.deleteuser)
2904 response = self.client.get(index_url)
2905 self.assertNotContains(response, articles)
2906
2907 # The app list displays Sections but not Articles as the latter has
2908 # ModelAdmin.has_module_permission() = False.
2909 self.client.force_login(self.superuser)
2910 response = self.client.get(reverse("admin7:app_list", args=("admin_views",)))
2911 self.assertContains(response, sections)
2912 self.assertNotContains(response, articles)
2913
2914 def test_post_save_message_no_forbidden_links_visible(self):
2915 """
2916 Post-save message shouldn't contain a link to the change form if the
2917 user doesn't have the change permission.
2918 """
2919 self.client.force_login(self.adduser)
2920 # Emulate Article creation for user with add-only permission.
2921 post_data = {
2922 "title": "Fun & games",
2923 "content": "Some content",
2924 "date_0": "2015-10-31",
2925 "date_1": "16:35:00",
2926 "_save": "Save",
2927 }
2928 response = self.client.post(
2929 reverse("admin:admin_views_article_add"), post_data, follow=True
2930 )
2931 self.assertContains(
2932 response,
2933 '<li class="success">The article “Fun & games” was added successfully.</li>',
2934 html=True,
2935 )
2936
2937
2938@override_settings(
2939 ROOT_URLCONF="admin_views.urls",
2940 TEMPLATES=[
2941 {
2942 "BACKEND": "django.template.backends.django.DjangoTemplates",
2943 "APP_DIRS": True,
2944 "OPTIONS": {
2945 "context_processors": [
2946 "django.contrib.auth.context_processors.auth",
2947 "django.contrib.messages.context_processors.messages",
2948 ],
2949 },
2950 }
2951 ],
2952)
2953class AdminViewProxyModelPermissionsTests(TestCase):
2954 """Tests for proxy models permissions in the admin."""
2955
2956 @classmethod
2957 def setUpTestData(cls):
2958 cls.viewuser = User.objects.create_user(
2959 username="viewuser", password="secret", is_staff=True
2960 )
2961 cls.adduser = User.objects.create_user(
2962 username="adduser", password="secret", is_staff=True
2963 )
2964 cls.changeuser = User.objects.create_user(
2965 username="changeuser", password="secret", is_staff=True
2966 )
2967 cls.deleteuser = User.objects.create_user(
2968 username="deleteuser", password="secret", is_staff=True
2969 )
2970 # Setup permissions.
2971 opts = UserProxy._meta
2972 cls.viewuser.user_permissions.add(
2973 get_perm(UserProxy, get_permission_codename("view", opts))
2974 )
2975 cls.adduser.user_permissions.add(
2976 get_perm(UserProxy, get_permission_codename("add", opts))
2977 )
2978 cls.changeuser.user_permissions.add(
2979 get_perm(UserProxy, get_permission_codename("change", opts))
2980 )
2981 cls.deleteuser.user_permissions.add(
2982 get_perm(UserProxy, get_permission_codename("delete", opts))
2983 )
2984 # UserProxy instances.
2985 cls.user_proxy = UserProxy.objects.create(
2986 username="user_proxy", password="secret"
2987 )
2988
2989 def test_add(self):
2990 self.client.force_login(self.adduser)
2991 url = reverse("admin:admin_views_userproxy_add")
2992 data = {
2993 "username": "can_add",
2994 "password": "secret",
2995 "date_joined_0": "2019-01-15",
2996 "date_joined_1": "16:59:10",
2997 }
2998 response = self.client.post(url, data, follow=True)
2999 self.assertEqual(response.status_code, 200)
3000 self.assertTrue(UserProxy.objects.filter(username="can_add").exists())
3001
3002 def test_view(self):
3003 self.client.force_login(self.viewuser)
3004 response = self.client.get(reverse("admin:admin_views_userproxy_changelist"))
3005 self.assertContains(response, "<h1>Select user proxy to view</h1>")
3006 self.assertEqual(response.status_code, 200)
3007 response = self.client.get(
3008 reverse("admin:admin_views_userproxy_change", args=(self.user_proxy.pk,))
3009 )
3010 self.assertContains(response, "<h1>View user proxy</h1>")
3011 self.assertContains(response, '<div class="readonly">user_proxy</div>')
3012
3013 def test_change(self):
3014 self.client.force_login(self.changeuser)
3015 data = {
3016 "password": self.user_proxy.password,
3017 "username": self.user_proxy.username,
3018 "date_joined_0": self.user_proxy.date_joined.strftime("%Y-%m-%d"),
3019 "date_joined_1": self.user_proxy.date_joined.strftime("%H:%M:%S"),
3020 "first_name": "first_name",
3021 }
3022 url = reverse("admin:admin_views_userproxy_change", args=(self.user_proxy.pk,))
3023 response = self.client.post(url, data)
3024 self.assertRedirects(
3025 response, reverse("admin:admin_views_userproxy_changelist")
3026 )
3027 self.assertEqual(
3028 UserProxy.objects.get(pk=self.user_proxy.pk).first_name, "first_name"
3029 )
3030
3031 def test_delete(self):
3032 self.client.force_login(self.deleteuser)
3033 url = reverse("admin:admin_views_userproxy_delete", args=(self.user_proxy.pk,))
3034 response = self.client.post(url, {"post": "yes"}, follow=True)
3035 self.assertEqual(response.status_code, 200)
3036 self.assertFalse(UserProxy.objects.filter(pk=self.user_proxy.pk).exists())
3037
3038
3039@override_settings(ROOT_URLCONF="admin_views.urls")
3040class AdminViewsNoUrlTest(TestCase):
3041 """Regression test for #17333"""
3042
3043 @classmethod
3044 def setUpTestData(cls):
3045 # User who can change Reports
3046 cls.changeuser = User.objects.create_user(
3047 username="changeuser", password="secret", is_staff=True
3048 )
3049 cls.changeuser.user_permissions.add(
3050 get_perm(Report, get_permission_codename("change", Report._meta))
3051 )
3052
3053 def test_no_standard_modeladmin_urls(self):
3054 """Admin index views don't break when user's ModelAdmin removes standard urls"""
3055 self.client.force_login(self.changeuser)
3056 r = self.client.get(reverse("admin:index"))
3057 # we shouldn't get a 500 error caused by a NoReverseMatch
3058 self.assertEqual(r.status_code, 200)
3059 self.client.get(reverse("admin:logout"))
3060
3061
3062@skipUnlessDBFeature("can_defer_constraint_checks")
3063@override_settings(ROOT_URLCONF="admin_views.urls")
3064class AdminViewDeletedObjectsTest(TestCase):
3065 @classmethod
3066 def setUpTestData(cls):
3067 cls.superuser = User.objects.create_superuser(
3068 username="super", password="secret", email="super@example.com"
3069 )
3070 cls.deleteuser = User.objects.create_user(
3071 username="deleteuser", password="secret", is_staff=True
3072 )
3073 cls.s1 = Section.objects.create(name="Test section")
3074 cls.a1 = Article.objects.create(
3075 content="<p>Middle content</p>",
3076 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3077 section=cls.s1,
3078 )
3079 cls.a2 = Article.objects.create(
3080 content="<p>Oldest content</p>",
3081 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3082 section=cls.s1,
3083 )
3084 cls.a3 = Article.objects.create(
3085 content="<p>Newest content</p>",
3086 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3087 section=cls.s1,
3088 )
3089 cls.p1 = PrePopulatedPost.objects.create(
3090 title="A Long Title", published=True, slug="a-long-title"
3091 )
3092
3093 cls.v1 = Villain.objects.create(name="Adam")
3094 cls.v2 = Villain.objects.create(name="Sue")
3095 cls.sv1 = SuperVillain.objects.create(name="Bob")
3096 cls.pl1 = Plot.objects.create(
3097 name="World Domination", team_leader=cls.v1, contact=cls.v2
3098 )
3099 cls.pl2 = Plot.objects.create(
3100 name="World Peace", team_leader=cls.v2, contact=cls.v2
3101 )
3102 cls.pl3 = Plot.objects.create(
3103 name="Corn Conspiracy", team_leader=cls.v1, contact=cls.v1
3104 )
3105 cls.pd1 = PlotDetails.objects.create(details="almost finished", plot=cls.pl1)
3106 cls.sh1 = SecretHideout.objects.create(
3107 location="underground bunker", villain=cls.v1
3108 )
3109 cls.sh2 = SecretHideout.objects.create(
3110 location="floating castle", villain=cls.sv1
3111 )
3112 cls.ssh1 = SuperSecretHideout.objects.create(
3113 location="super floating castle!", supervillain=cls.sv1
3114 )
3115 cls.cy1 = CyclicOne.objects.create(name="I am recursive", two_id=1)
3116 cls.cy2 = CyclicTwo.objects.create(name="I am recursive too", one_id=1)
3117
3118 def setUp(self):
3119 self.client.force_login(self.superuser)
3120
3121 def test_nesting(self):
3122 """
3123 Objects should be nested to display the relationships that
3124 cause them to be scheduled for deletion.
3125 """
3126 pattern = re.compile(
3127 r'<li>Plot: <a href="%s">World Domination</a>\s*<ul>\s*'
3128 r'<li>Plot details: <a href="%s">almost finished</a>'
3129 % (
3130 reverse("admin:admin_views_plot_change", args=(self.pl1.pk,)),
3131 reverse("admin:admin_views_plotdetails_change", args=(self.pd1.pk,)),
3132 )
3133 )
3134 response = self.client.get(
3135 reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3136 )
3137 self.assertRegex(response.content.decode(), pattern)
3138
3139 def test_cyclic(self):
3140 """
3141 Cyclic relationships should still cause each object to only be
3142 listed once.
3143 """
3144 one = '<li>Cyclic one: <a href="%s">I am recursive</a>' % (
3145 reverse("admin:admin_views_cyclicone_change", args=(self.cy1.pk,)),
3146 )
3147 two = '<li>Cyclic two: <a href="%s">I am recursive too</a>' % (
3148 reverse("admin:admin_views_cyclictwo_change", args=(self.cy2.pk,)),
3149 )
3150 response = self.client.get(
3151 reverse("admin:admin_views_cyclicone_delete", args=(self.cy1.pk,))
3152 )
3153
3154 self.assertContains(response, one, 1)
3155 self.assertContains(response, two, 1)
3156
3157 def test_perms_needed(self):
3158 self.client.logout()
3159 delete_user = User.objects.get(username="deleteuser")
3160 delete_user.user_permissions.add(
3161 get_perm(Plot, get_permission_codename("delete", Plot._meta))
3162 )
3163
3164 self.client.force_login(self.deleteuser)
3165 response = self.client.get(
3166 reverse("admin:admin_views_plot_delete", args=(self.pl1.pk,))
3167 )
3168 self.assertContains(
3169 response,
3170 "your account doesn't have permission to delete the following types of objects",
3171 )
3172 self.assertContains(response, "<li>plot details</li>")
3173
3174 def test_protected(self):
3175 q = Question.objects.create(question="Why?")
3176 a1 = Answer.objects.create(question=q, answer="Because.")
3177 a2 = Answer.objects.create(question=q, answer="Yes.")
3178
3179 response = self.client.get(
3180 reverse("admin:admin_views_question_delete", args=(q.pk,))
3181 )
3182 self.assertContains(
3183 response, "would require deleting the following protected related objects"
3184 )
3185 self.assertContains(
3186 response,
3187 '<li>Answer: <a href="%s">Because.</a></li>'
3188 % reverse("admin:admin_views_answer_change", args=(a1.pk,)),
3189 )
3190 self.assertContains(
3191 response,
3192 '<li>Answer: <a href="%s">Yes.</a></li>'
3193 % reverse("admin:admin_views_answer_change", args=(a2.pk,)),
3194 )
3195
3196 def test_post_delete_protected(self):
3197 """
3198 A POST request to delete protected objects should display the page
3199 which says the deletion is prohibited.
3200 """
3201 q = Question.objects.create(question="Why?")
3202 Answer.objects.create(question=q, answer="Because.")
3203
3204 response = self.client.post(
3205 reverse("admin:admin_views_question_delete", args=(q.pk,)), {"post": "yes"}
3206 )
3207 self.assertEqual(Question.objects.count(), 1)
3208 self.assertContains(
3209 response, "would require deleting the following protected related objects"
3210 )
3211
3212 def test_restricted(self):
3213 album = Album.objects.create(title="Amaryllis")
3214 song = Song.objects.create(album=album, name="Unity")
3215 response = self.client.get(
3216 reverse("admin:admin_views_album_delete", args=(album.pk,))
3217 )
3218 self.assertContains(
3219 response, "would require deleting the following protected related objects",
3220 )
3221 self.assertContains(
3222 response,
3223 '<li>Song: <a href="%s">Unity</a></li>'
3224 % reverse("admin:admin_views_song_change", args=(song.pk,)),
3225 )
3226
3227 def test_post_delete_restricted(self):
3228 album = Album.objects.create(title="Amaryllis")
3229 Song.objects.create(album=album, name="Unity")
3230 response = self.client.post(
3231 reverse("admin:admin_views_album_delete", args=(album.pk,)),
3232 {"post": "yes"},
3233 )
3234 self.assertEqual(Album.objects.count(), 1)
3235 self.assertContains(
3236 response, "would require deleting the following protected related objects",
3237 )
3238
3239 def test_not_registered(self):
3240 should_contain = """<li>Secret hideout: underground bunker"""
3241 response = self.client.get(
3242 reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3243 )
3244 self.assertContains(response, should_contain, 1)
3245
3246 def test_multiple_fkeys_to_same_model(self):
3247 """
3248 If a deleted object has two relationships from another model,
3249 both of those should be followed in looking for related
3250 objects to delete.
3251 """
3252 should_contain = '<li>Plot: <a href="%s">World Domination</a>' % reverse(
3253 "admin:admin_views_plot_change", args=(self.pl1.pk,)
3254 )
3255 response = self.client.get(
3256 reverse("admin:admin_views_villain_delete", args=(self.v1.pk,))
3257 )
3258 self.assertContains(response, should_contain)
3259 response = self.client.get(
3260 reverse("admin:admin_views_villain_delete", args=(self.v2.pk,))
3261 )
3262 self.assertContains(response, should_contain)
3263
3264 def test_multiple_fkeys_to_same_instance(self):
3265 """
3266 If a deleted object has two relationships pointing to it from
3267 another object, the other object should still only be listed
3268 once.
3269 """
3270 should_contain = '<li>Plot: <a href="%s">World Peace</a></li>' % reverse(
3271 "admin:admin_views_plot_change", args=(self.pl2.pk,)
3272 )
3273 response = self.client.get(
3274 reverse("admin:admin_views_villain_delete", args=(self.v2.pk,))
3275 )
3276 self.assertContains(response, should_contain, 1)
3277
3278 def test_inheritance(self):
3279 """
3280 In the case of an inherited model, if either the child or
3281 parent-model instance is deleted, both instances are listed
3282 for deletion, as well as any relationships they have.
3283 """
3284 should_contain = [
3285 '<li>Villain: <a href="%s">Bob</a>'
3286 % reverse("admin:admin_views_villain_change", args=(self.sv1.pk,)),
3287 '<li>Super villain: <a href="%s">Bob</a>'
3288 % reverse("admin:admin_views_supervillain_change", args=(self.sv1.pk,)),
3289 "<li>Secret hideout: floating castle",
3290 "<li>Super secret hideout: super floating castle!",
3291 ]
3292 response = self.client.get(
3293 reverse("admin:admin_views_villain_delete", args=(self.sv1.pk,))
3294 )
3295 for should in should_contain:
3296 self.assertContains(response, should, 1)
3297 response = self.client.get(
3298 reverse("admin:admin_views_supervillain_delete", args=(self.sv1.pk,))
3299 )
3300 for should in should_contain:
3301 self.assertContains(response, should, 1)
3302
3303 def test_generic_relations(self):
3304 """
3305 If a deleted object has GenericForeignKeys pointing to it,
3306 those objects should be listed for deletion.
3307 """
3308 plot = self.pl3
3309 tag = FunkyTag.objects.create(content_object=plot, name="hott")
3310 should_contain = '<li>Funky tag: <a href="%s">hott' % reverse(
3311 "admin:admin_views_funkytag_change", args=(tag.id,)
3312 )
3313 response = self.client.get(
3314 reverse("admin:admin_views_plot_delete", args=(plot.pk,))
3315 )
3316 self.assertContains(response, should_contain)
3317
3318 def test_generic_relations_with_related_query_name(self):
3319 """
3320 If a deleted object has GenericForeignKey with
3321 GenericRelation(related_query_name='...') pointing to it, those objects
3322 should be listed for deletion.
3323 """
3324 bookmark = Bookmark.objects.create(name="djangoproject")
3325 tag = FunkyTag.objects.create(content_object=bookmark, name="django")
3326 tag_url = reverse("admin:admin_views_funkytag_change", args=(tag.id,))
3327 should_contain = '<li>Funky tag: <a href="%s">django' % tag_url
3328 response = self.client.get(
3329 reverse("admin:admin_views_bookmark_delete", args=(bookmark.pk,))
3330 )
3331 self.assertContains(response, should_contain)
3332
3333 def test_delete_view_uses_get_deleted_objects(self):
3334 """The delete view uses ModelAdmin.get_deleted_objects()."""
3335 book = Book.objects.create(name="Test Book")
3336 response = self.client.get(
3337 reverse("admin2:admin_views_book_delete", args=(book.pk,))
3338 )
3339 # BookAdmin.get_deleted_objects() returns custom text.
3340 self.assertContains(response, "a deletable object")
3341
3342
3343@override_settings(ROOT_URLCONF="admin_views.urls")
3344class TestGenericRelations(TestCase):
3345 @classmethod
3346 def setUpTestData(cls):
3347 cls.superuser = User.objects.create_superuser(
3348 username="super", password="secret", email="super@example.com"
3349 )
3350 cls.v1 = Villain.objects.create(name="Adam")
3351 cls.pl3 = Plot.objects.create(
3352 name="Corn Conspiracy", team_leader=cls.v1, contact=cls.v1
3353 )
3354
3355 def setUp(self):
3356 self.client.force_login(self.superuser)
3357
3358 def test_generic_content_object_in_list_display(self):
3359 FunkyTag.objects.create(content_object=self.pl3, name="hott")
3360 response = self.client.get(reverse("admin:admin_views_funkytag_changelist"))
3361 self.assertContains(response, "%s</td>" % self.pl3)
3362
3363
3364@override_settings(ROOT_URLCONF="admin_views.urls")
3365class AdminViewStringPrimaryKeyTest(TestCase):
3366 @classmethod
3367 def setUpTestData(cls):
3368 cls.superuser = User.objects.create_superuser(
3369 username="super", password="secret", email="super@example.com"
3370 )
3371 cls.s1 = Section.objects.create(name="Test section")
3372 cls.a1 = Article.objects.create(
3373 content="<p>Middle content</p>",
3374 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3375 section=cls.s1,
3376 )
3377 cls.a2 = Article.objects.create(
3378 content="<p>Oldest content</p>",
3379 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3380 section=cls.s1,
3381 )
3382 cls.a3 = Article.objects.create(
3383 content="<p>Newest content</p>",
3384 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3385 section=cls.s1,
3386 )
3387 cls.p1 = PrePopulatedPost.objects.create(
3388 title="A Long Title", published=True, slug="a-long-title"
3389 )
3390 cls.pk = (
3391 "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 "
3392 r"""-_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`"""
3393 )
3394 cls.m1 = ModelWithStringPrimaryKey.objects.create(string_pk=cls.pk)
3395 content_type_pk = ContentType.objects.get_for_model(
3396 ModelWithStringPrimaryKey
3397 ).pk
3398 user_pk = cls.superuser.pk
3399 LogEntry.objects.log_action(
3400 user_pk,
3401 content_type_pk,
3402 cls.pk,
3403 cls.pk,
3404 2,
3405 change_message="Changed something",
3406 )
3407
3408 def setUp(self):
3409 self.client.force_login(self.superuser)
3410
3411 def test_get_history_view(self):
3412 """
3413 Retrieving the history for an object using urlencoded form of primary
3414 key should work.
3415 Refs #12349, #18550.
3416 """
3417 response = self.client.get(
3418 reverse(
3419 "admin:admin_views_modelwithstringprimarykey_history", args=(self.pk,)
3420 )
3421 )
3422 self.assertContains(response, escape(self.pk))
3423 self.assertContains(response, "Changed something")
3424 self.assertEqual(response.status_code, 200)
3425
3426 def test_get_change_view(self):
3427 "Retrieving the object using urlencoded form of primary key should work"
3428 response = self.client.get(
3429 reverse(
3430 "admin:admin_views_modelwithstringprimarykey_change", args=(self.pk,)
3431 )
3432 )
3433 self.assertContains(response, escape(self.pk))
3434 self.assertEqual(response.status_code, 200)
3435
3436 def test_changelist_to_changeform_link(self):
3437 "Link to the changeform of the object in changelist should use reverse() and be quoted -- #18072"
3438 response = self.client.get(
3439 reverse("admin:admin_views_modelwithstringprimarykey_changelist")
3440 )
3441 # this URL now comes through reverse(), thus url quoting and iri_to_uri encoding
3442 pk_final_url = escape(iri_to_uri(quote(self.pk)))
3443 change_url = reverse(
3444 "admin:admin_views_modelwithstringprimarykey_change", args=("__fk__",)
3445 ).replace("__fk__", pk_final_url)
3446 should_contain = '<th class="field-__str__"><a href="%s">%s</a></th>' % (
3447 change_url,
3448 escape(self.pk),
3449 )
3450 self.assertContains(response, should_contain)
3451
3452 def test_recentactions_link(self):
3453 "The link from the recent actions list referring to the changeform of the object should be quoted"
3454 response = self.client.get(reverse("admin:index"))
3455 link = reverse(
3456 "admin:admin_views_modelwithstringprimarykey_change", args=(quote(self.pk),)
3457 )
3458 should_contain = """<a href="%s">%s</a>""" % (escape(link), escape(self.pk))
3459 self.assertContains(response, should_contain)
3460
3461 def test_deleteconfirmation_link(self):
3462 "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
3463 url = reverse(
3464 "admin:admin_views_modelwithstringprimarykey_delete", args=(quote(self.pk),)
3465 )
3466 response = self.client.get(url)
3467 # this URL now comes through reverse(), thus url quoting and iri_to_uri encoding
3468 change_url = reverse(
3469 "admin:admin_views_modelwithstringprimarykey_change", args=("__fk__",)
3470 ).replace("__fk__", escape(iri_to_uri(quote(self.pk))))
3471 should_contain = '<a href="%s">%s</a>' % (change_url, escape(self.pk))
3472 self.assertContains(response, should_contain)
3473
3474 def test_url_conflicts_with_add(self):
3475 "A model with a primary key that ends with add or is `add` should be visible"
3476 add_model = ModelWithStringPrimaryKey.objects.create(
3477 pk="i have something to add"
3478 )
3479 add_model.save()
3480 response = self.client.get(
3481 reverse(
3482 "admin:admin_views_modelwithstringprimarykey_change",
3483 args=(quote(add_model.pk),),
3484 )
3485 )
3486 should_contain = """<h1>Change model with string primary key</h1>"""
3487 self.assertContains(response, should_contain)
3488
3489 add_model2 = ModelWithStringPrimaryKey.objects.create(pk="add")
3490 add_url = reverse("admin:admin_views_modelwithstringprimarykey_add")
3491 change_url = reverse(
3492 "admin:admin_views_modelwithstringprimarykey_change",
3493 args=(quote(add_model2.pk),),
3494 )
3495 self.assertNotEqual(add_url, change_url)
3496
3497 def test_url_conflicts_with_delete(self):
3498 "A model with a primary key that ends with delete should be visible"
3499 delete_model = ModelWithStringPrimaryKey(pk="delete")
3500 delete_model.save()
3501 response = self.client.get(
3502 reverse(
3503 "admin:admin_views_modelwithstringprimarykey_change",
3504 args=(quote(delete_model.pk),),
3505 )
3506 )
3507 should_contain = """<h1>Change model with string primary key</h1>"""
3508 self.assertContains(response, should_contain)
3509
3510 def test_url_conflicts_with_history(self):
3511 "A model with a primary key that ends with history should be visible"
3512 history_model = ModelWithStringPrimaryKey(pk="history")
3513 history_model.save()
3514 response = self.client.get(
3515 reverse(
3516 "admin:admin_views_modelwithstringprimarykey_change",
3517 args=(quote(history_model.pk),),
3518 )
3519 )
3520 should_contain = """<h1>Change model with string primary key</h1>"""
3521 self.assertContains(response, should_contain)
3522
3523 def test_shortcut_view_with_escaping(self):
3524 "'View on site should' work properly with char fields"
3525 model = ModelWithStringPrimaryKey(pk="abc_123")
3526 model.save()
3527 response = self.client.get(
3528 reverse(
3529 "admin:admin_views_modelwithstringprimarykey_change",
3530 args=(quote(model.pk),),
3531 )
3532 )
3533 should_contain = '/%s/" class="viewsitelink">' % model.pk
3534 self.assertContains(response, should_contain)
3535
3536 def test_change_view_history_link(self):
3537 """Object history button link should work and contain the pk value quoted."""
3538 url = reverse(
3539 "admin:%s_modelwithstringprimarykey_change"
3540 % ModelWithStringPrimaryKey._meta.app_label,
3541 args=(quote(self.pk),),
3542 )
3543 response = self.client.get(url)
3544 self.assertEqual(response.status_code, 200)
3545 expected_link = reverse(
3546 "admin:%s_modelwithstringprimarykey_history"
3547 % ModelWithStringPrimaryKey._meta.app_label,
3548 args=(quote(self.pk),),
3549 )
3550 self.assertContains(
3551 response, '<a href="%s" class="historylink"' % escape(expected_link)
3552 )
3553
3554 def test_redirect_on_add_view_continue_button(self):
3555 """As soon as an object is added using "Save and continue editing"
3556 button, the user should be redirected to the object's change_view.
3557
3558 In case primary key is a string containing some special characters
3559 like slash or underscore, these characters must be escaped (see #22266)
3560 """
3561 response = self.client.post(
3562 reverse("admin:admin_views_modelwithstringprimarykey_add"),
3563 {
3564 "string_pk": "123/history",
3565 "_continue": "1", # Save and continue editing
3566 },
3567 )
3568
3569 self.assertEqual(response.status_code, 302) # temporary redirect
3570 self.assertIn("/123_2Fhistory/", response["location"]) # PK is quoted
3571
3572
3573@override_settings(ROOT_URLCONF="admin_views.urls")
3574class SecureViewTests(TestCase):
3575 """
3576 Test behavior of a view protected by the staff_member_required decorator.
3577 """
3578
3579 def test_secure_view_shows_login_if_not_logged_in(self):
3580 secure_url = reverse("secure_view")
3581 response = self.client.get(secure_url)
3582 self.assertRedirects(
3583 response, "%s?next=%s" % (reverse("admin:login"), secure_url)
3584 )
3585 response = self.client.get(secure_url, follow=True)
3586 self.assertTemplateUsed(response, "admin/login.html")
3587 self.assertEqual(response.context[REDIRECT_FIELD_NAME], secure_url)
3588
3589 def test_staff_member_required_decorator_works_with_argument(self):
3590 """
3591 Staff_member_required decorator works with an argument
3592 (redirect_field_name).
3593 """
3594 secure_url = "/test_admin/admin/secure-view2/"
3595 response = self.client.get(secure_url)
3596 self.assertRedirects(
3597 response, "%s?myfield=%s" % (reverse("admin:login"), secure_url)
3598 )
3599
3600
3601@override_settings(ROOT_URLCONF="admin_views.urls")
3602class AdminViewUnicodeTest(TestCase):
3603 @classmethod
3604 def setUpTestData(cls):
3605 cls.superuser = User.objects.create_superuser(
3606 username="super", password="secret", email="super@example.com"
3607 )
3608 cls.b1 = Book.objects.create(name="Lærdommer")
3609 cls.p1 = Promo.objects.create(name="<Promo for Lærdommer>", book=cls.b1)
3610 cls.chap1 = Chapter.objects.create(
3611 title="Norske bostaver æøå skaper problemer",
3612 content="<p>Svært frustrerende med UnicodeDecodeErro</p>",
3613 book=cls.b1,
3614 )
3615 cls.chap2 = Chapter.objects.create(
3616 title="Kjærlighet",
3617 content="<p>La kjærligheten til de lidende seire.</p>",
3618 book=cls.b1,
3619 )
3620 cls.chap3 = Chapter.objects.create(
3621 title="Kjærlighet", content="<p>Noe innhold</p>", book=cls.b1
3622 )
3623 cls.chap4 = ChapterXtra1.objects.create(
3624 chap=cls.chap1, xtra="<Xtra(1) Norske bostaver æøå skaper problemer>"
3625 )
3626 cls.chap5 = ChapterXtra1.objects.create(
3627 chap=cls.chap2, xtra="<Xtra(1) Kjærlighet>"
3628 )
3629 cls.chap6 = ChapterXtra1.objects.create(
3630 chap=cls.chap3, xtra="<Xtra(1) Kjærlighet>"
3631 )
3632 cls.chap7 = ChapterXtra2.objects.create(
3633 chap=cls.chap1, xtra="<Xtra(2) Norske bostaver æøå skaper problemer>"
3634 )
3635 cls.chap8 = ChapterXtra2.objects.create(
3636 chap=cls.chap2, xtra="<Xtra(2) Kjærlighet>"
3637 )
3638 cls.chap9 = ChapterXtra2.objects.create(
3639 chap=cls.chap3, xtra="<Xtra(2) Kjærlighet>"
3640 )
3641
3642 def setUp(self):
3643 self.client.force_login(self.superuser)
3644
3645 def test_unicode_edit(self):
3646 """
3647 A test to ensure that POST on edit_view handles non-ASCII characters.
3648 """
3649 post_data = {
3650 "name": "Test lærdommer",
3651 # inline data
3652 "chapter_set-TOTAL_FORMS": "6",
3653 "chapter_set-INITIAL_FORMS": "3",
3654 "chapter_set-MAX_NUM_FORMS": "0",
3655 "chapter_set-0-id": self.chap1.pk,
3656 "chapter_set-0-title": "Norske bostaver æøå skaper problemer",
3657 "chapter_set-0-content": "<p>Svært frustrerende med UnicodeDecodeError</p>",
3658 "chapter_set-1-id": self.chap2.id,
3659 "chapter_set-1-title": "Kjærlighet.",
3660 "chapter_set-1-content": "<p>La kjærligheten til de lidende seire.</p>",
3661 "chapter_set-2-id": self.chap3.id,
3662 "chapter_set-2-title": "Need a title.",
3663 "chapter_set-2-content": "<p>Newest content</p>",
3664 "chapter_set-3-id": "",
3665 "chapter_set-3-title": "",
3666 "chapter_set-3-content": "",
3667 "chapter_set-4-id": "",
3668 "chapter_set-4-title": "",
3669 "chapter_set-4-content": "",
3670 "chapter_set-5-id": "",
3671 "chapter_set-5-title": "",
3672 "chapter_set-5-content": "",
3673 }
3674
3675 response = self.client.post(
3676 reverse("admin:admin_views_book_change", args=(self.b1.pk,)), post_data
3677 )
3678 self.assertEqual(response.status_code, 302) # redirect somewhere
3679
3680 def test_unicode_delete(self):
3681 """
3682 The delete_view handles non-ASCII characters
3683 """
3684 delete_dict = {"post": "yes"}
3685 delete_url = reverse("admin:admin_views_book_delete", args=(self.b1.pk,))
3686 response = self.client.get(delete_url)
3687 self.assertEqual(response.status_code, 200)
3688 response = self.client.post(delete_url, delete_dict)
3689 self.assertRedirects(response, reverse("admin:admin_views_book_changelist"))
3690
3691
3692@override_settings(ROOT_URLCONF="admin_views.urls")
3693class AdminViewListEditable(TestCase):
3694 @classmethod
3695 def setUpTestData(cls):
3696 cls.superuser = User.objects.create_superuser(
3697 username="super", password="secret", email="super@example.com"
3698 )
3699 cls.s1 = Section.objects.create(name="Test section")
3700 cls.a1 = Article.objects.create(
3701 content="<p>Middle content</p>",
3702 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
3703 section=cls.s1,
3704 )
3705 cls.a2 = Article.objects.create(
3706 content="<p>Oldest content</p>",
3707 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
3708 section=cls.s1,
3709 )
3710 cls.a3 = Article.objects.create(
3711 content="<p>Newest content</p>",
3712 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
3713 section=cls.s1,
3714 )
3715 cls.p1 = PrePopulatedPost.objects.create(
3716 title="A Long Title", published=True, slug="a-long-title"
3717 )
3718 cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
3719 cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
3720 cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
3721
3722 def setUp(self):
3723 self.client.force_login(self.superuser)
3724
3725 def test_inheritance(self):
3726 Podcast.objects.create(
3727 name="This Week in Django", release_date=datetime.date.today()
3728 )
3729 response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
3730 self.assertEqual(response.status_code, 200)
3731
3732 def test_inheritance_2(self):
3733 Vodcast.objects.create(name="This Week in Django", released=True)
3734 response = self.client.get(reverse("admin:admin_views_vodcast_changelist"))
3735 self.assertEqual(response.status_code, 200)
3736
3737 def test_custom_pk(self):
3738 Language.objects.create(iso="en", name="English", english_name="English")
3739 response = self.client.get(reverse("admin:admin_views_language_changelist"))
3740 self.assertEqual(response.status_code, 200)
3741
3742 def test_changelist_input_html(self):
3743 response = self.client.get(reverse("admin:admin_views_person_changelist"))
3744 # 2 inputs per object(the field and the hidden id field) = 6
3745 # 4 management hidden fields = 4
3746 # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
3747 # main form submit button = 1
3748 # search field and search submit button = 2
3749 # CSRF field = 1
3750 # field to track 'select all' across paginated views = 1
3751 # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
3752 self.assertContains(response, "<input", count=19)
3753 # 1 select per object = 3 selects
3754 self.assertContains(response, "<select", count=4)
3755
3756 def test_post_messages(self):
3757 # Ticket 12707: Saving inline editable should not show admin
3758 # action warnings
3759 data = {
3760 "form-TOTAL_FORMS": "3",
3761 "form-INITIAL_FORMS": "3",
3762 "form-MAX_NUM_FORMS": "0",
3763 "form-0-gender": "1",
3764 "form-0-id": "%s" % self.per1.pk,
3765 "form-1-gender": "2",
3766 "form-1-id": "%s" % self.per2.pk,
3767 "form-2-alive": "checked",
3768 "form-2-gender": "1",
3769 "form-2-id": "%s" % self.per3.pk,
3770 "_save": "Save",
3771 }
3772 response = self.client.post(
3773 reverse("admin:admin_views_person_changelist"), data, follow=True
3774 )
3775 self.assertEqual(len(response.context["messages"]), 1)
3776
3777 def test_post_submission(self):
3778 data = {
3779 "form-TOTAL_FORMS": "3",
3780 "form-INITIAL_FORMS": "3",
3781 "form-MAX_NUM_FORMS": "0",
3782 "form-0-gender": "1",
3783 "form-0-id": "%s" % self.per1.pk,
3784 "form-1-gender": "2",
3785 "form-1-id": "%s" % self.per2.pk,
3786 "form-2-alive": "checked",
3787 "form-2-gender": "1",
3788 "form-2-id": "%s" % self.per3.pk,
3789 "_save": "Save",
3790 }
3791 self.client.post(reverse("admin:admin_views_person_changelist"), data)
3792
3793 self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
3794 self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
3795
3796 # test a filtered page
3797 data = {
3798 "form-TOTAL_FORMS": "2",
3799 "form-INITIAL_FORMS": "2",
3800 "form-MAX_NUM_FORMS": "0",
3801 "form-0-id": "%s" % self.per1.pk,
3802 "form-0-gender": "1",
3803 "form-0-alive": "checked",
3804 "form-1-id": "%s" % self.per3.pk,
3805 "form-1-gender": "1",
3806 "form-1-alive": "checked",
3807 "_save": "Save",
3808 }
3809 self.client.post(
3810 reverse("admin:admin_views_person_changelist") + "?gender__exact=1", data
3811 )
3812
3813 self.assertIs(Person.objects.get(name="John Mauchly").alive, True)
3814
3815 # test a searched page
3816 data = {
3817 "form-TOTAL_FORMS": "1",
3818 "form-INITIAL_FORMS": "1",
3819 "form-MAX_NUM_FORMS": "0",
3820 "form-0-id": "%s" % self.per1.pk,
3821 "form-0-gender": "1",
3822 "_save": "Save",
3823 }
3824 self.client.post(
3825 reverse("admin:admin_views_person_changelist") + "?q=john", data
3826 )
3827
3828 self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
3829
3830 def test_non_field_errors(self):
3831 """
3832 Non-field errors are displayed for each of the forms in the
3833 changelist's formset.
3834 """
3835 fd1 = FoodDelivery.objects.create(
3836 reference="123", driver="bill", restaurant="thai"
3837 )
3838 fd2 = FoodDelivery.objects.create(
3839 reference="456", driver="bill", restaurant="india"
3840 )
3841 fd3 = FoodDelivery.objects.create(
3842 reference="789", driver="bill", restaurant="pizza"
3843 )
3844
3845 data = {
3846 "form-TOTAL_FORMS": "3",
3847 "form-INITIAL_FORMS": "3",
3848 "form-MAX_NUM_FORMS": "0",
3849 "form-0-id": str(fd1.id),
3850 "form-0-reference": "123",
3851 "form-0-driver": "bill",
3852 "form-0-restaurant": "thai",
3853 # Same data as above: Forbidden because of unique_together!
3854 "form-1-id": str(fd2.id),
3855 "form-1-reference": "456",
3856 "form-1-driver": "bill",
3857 "form-1-restaurant": "thai",
3858 "form-2-id": str(fd3.id),
3859 "form-2-reference": "789",
3860 "form-2-driver": "bill",
3861 "form-2-restaurant": "pizza",
3862 "_save": "Save",
3863 }
3864 response = self.client.post(
3865 reverse("admin:admin_views_fooddelivery_changelist"), data
3866 )
3867 self.assertContains(
3868 response,
3869 '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery '
3870 "with this Driver and Restaurant already exists.</li></ul></td></tr>",
3871 1,
3872 html=True,
3873 )
3874
3875 data = {
3876 "form-TOTAL_FORMS": "3",
3877 "form-INITIAL_FORMS": "3",
3878 "form-MAX_NUM_FORMS": "0",
3879 "form-0-id": str(fd1.id),
3880 "form-0-reference": "123",
3881 "form-0-driver": "bill",
3882 "form-0-restaurant": "thai",
3883 # Same data as above: Forbidden because of unique_together!
3884 "form-1-id": str(fd2.id),
3885 "form-1-reference": "456",
3886 "form-1-driver": "bill",
3887 "form-1-restaurant": "thai",
3888 # Same data also.
3889 "form-2-id": str(fd3.id),
3890 "form-2-reference": "789",
3891 "form-2-driver": "bill",
3892 "form-2-restaurant": "thai",
3893 "_save": "Save",
3894 }
3895 response = self.client.post(
3896 reverse("admin:admin_views_fooddelivery_changelist"), data
3897 )
3898 self.assertContains(
3899 response,
3900 '<tr><td colspan="4"><ul class="errorlist nonfield"><li>Food delivery '
3901 "with this Driver and Restaurant already exists.</li></ul></td></tr>",
3902 2,
3903 html=True,
3904 )
3905
3906 def test_non_form_errors(self):
3907 # test if non-form errors are handled; ticket #12716
3908 data = {
3909 "form-TOTAL_FORMS": "1",
3910 "form-INITIAL_FORMS": "1",
3911 "form-MAX_NUM_FORMS": "0",
3912 "form-0-id": "%s" % self.per2.pk,
3913 "form-0-alive": "1",
3914 "form-0-gender": "2",
3915 # The form processing understands this as a list_editable "Save"
3916 # and not an action "Go".
3917 "_save": "Save",
3918 }
3919 response = self.client.post(
3920 reverse("admin:admin_views_person_changelist"), data
3921 )
3922 self.assertContains(response, "Grace is not a Zombie")
3923
3924 def test_non_form_errors_is_errorlist(self):
3925 # test if non-form errors are correctly handled; ticket #12878
3926 data = {
3927 "form-TOTAL_FORMS": "1",
3928 "form-INITIAL_FORMS": "1",
3929 "form-MAX_NUM_FORMS": "0",
3930 "form-0-id": "%s" % self.per2.pk,
3931 "form-0-alive": "1",
3932 "form-0-gender": "2",
3933 "_save": "Save",
3934 }
3935 response = self.client.post(
3936 reverse("admin:admin_views_person_changelist"), data
3937 )
3938 non_form_errors = response.context["cl"].formset.non_form_errors()
3939 self.assertIsInstance(non_form_errors, ErrorList)
3940 self.assertEqual(
3941 str(non_form_errors), str(ErrorList(["Grace is not a Zombie"]))
3942 )
3943
3944 def test_list_editable_ordering(self):
3945 collector = Collector.objects.create(id=1, name="Frederick Clegg")
3946
3947 Category.objects.create(id=1, order=1, collector=collector)
3948 Category.objects.create(id=2, order=2, collector=collector)
3949 Category.objects.create(id=3, order=0, collector=collector)
3950 Category.objects.create(id=4, order=0, collector=collector)
3951
3952 # NB: The order values must be changed so that the items are reordered.
3953 data = {
3954 "form-TOTAL_FORMS": "4",
3955 "form-INITIAL_FORMS": "4",
3956 "form-MAX_NUM_FORMS": "0",
3957 "form-0-order": "14",
3958 "form-0-id": "1",
3959 "form-0-collector": "1",
3960 "form-1-order": "13",
3961 "form-1-id": "2",
3962 "form-1-collector": "1",
3963 "form-2-order": "1",
3964 "form-2-id": "3",
3965 "form-2-collector": "1",
3966 "form-3-order": "0",
3967 "form-3-id": "4",
3968 "form-3-collector": "1",
3969 # The form processing understands this as a list_editable "Save"
3970 # and not an action "Go".
3971 "_save": "Save",
3972 }
3973 response = self.client.post(
3974 reverse("admin:admin_views_category_changelist"), data
3975 )
3976 # Successful post will redirect
3977 self.assertEqual(response.status_code, 302)
3978
3979 # The order values have been applied to the right objects
3980 self.assertEqual(Category.objects.get(id=1).order, 14)
3981 self.assertEqual(Category.objects.get(id=2).order, 13)
3982 self.assertEqual(Category.objects.get(id=3).order, 1)
3983 self.assertEqual(Category.objects.get(id=4).order, 0)
3984
3985 def test_list_editable_pagination(self):
3986 """
3987 Pagination works for list_editable items.
3988 """
3989 UnorderedObject.objects.create(id=1, name="Unordered object #1")
3990 UnorderedObject.objects.create(id=2, name="Unordered object #2")
3991 UnorderedObject.objects.create(id=3, name="Unordered object #3")
3992 response = self.client.get(
3993 reverse("admin:admin_views_unorderedobject_changelist")
3994 )
3995 self.assertContains(response, "Unordered object #3")
3996 self.assertContains(response, "Unordered object #2")
3997 self.assertNotContains(response, "Unordered object #1")
3998 response = self.client.get(
3999 reverse("admin:admin_views_unorderedobject_changelist") + "?p=1"
4000 )
4001 self.assertNotContains(response, "Unordered object #3")
4002 self.assertNotContains(response, "Unordered object #2")
4003 self.assertContains(response, "Unordered object #1")
4004
4005 def test_list_editable_action_submit(self):
4006 # List editable changes should not be executed if the action "Go" button is
4007 # used to submit the form.
4008 data = {
4009 "form-TOTAL_FORMS": "3",
4010 "form-INITIAL_FORMS": "3",
4011 "form-MAX_NUM_FORMS": "0",
4012 "form-0-gender": "1",
4013 "form-0-id": "1",
4014 "form-1-gender": "2",
4015 "form-1-id": "2",
4016 "form-2-alive": "checked",
4017 "form-2-gender": "1",
4018 "form-2-id": "3",
4019 "index": "0",
4020 "_selected_action": ["3"],
4021 "action": ["", "delete_selected"],
4022 }
4023 self.client.post(reverse("admin:admin_views_person_changelist"), data)
4024
4025 self.assertIs(Person.objects.get(name="John Mauchly").alive, True)
4026 self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 1)
4027
4028 def test_list_editable_action_choices(self):
4029 # List editable changes should be executed if the "Save" button is
4030 # used to submit the form - any action choices should be ignored.
4031 data = {
4032 "form-TOTAL_FORMS": "3",
4033 "form-INITIAL_FORMS": "3",
4034 "form-MAX_NUM_FORMS": "0",
4035 "form-0-gender": "1",
4036 "form-0-id": "%s" % self.per1.pk,
4037 "form-1-gender": "2",
4038 "form-1-id": "%s" % self.per2.pk,
4039 "form-2-alive": "checked",
4040 "form-2-gender": "1",
4041 "form-2-id": "%s" % self.per3.pk,
4042 "_save": "Save",
4043 "_selected_action": ["1"],
4044 "action": ["", "delete_selected"],
4045 }
4046 self.client.post(reverse("admin:admin_views_person_changelist"), data)
4047
4048 self.assertIs(Person.objects.get(name="John Mauchly").alive, False)
4049 self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2)
4050
4051 def test_list_editable_popup(self):
4052 """
4053 Fields should not be list-editable in popups.
4054 """
4055 response = self.client.get(reverse("admin:admin_views_person_changelist"))
4056 self.assertNotEqual(response.context["cl"].list_editable, ())
4057 response = self.client.get(
4058 reverse("admin:admin_views_person_changelist") + "?%s" % IS_POPUP_VAR
4059 )
4060 self.assertEqual(response.context["cl"].list_editable, ())
4061
4062 def test_pk_hidden_fields(self):
4063 """
4064 hidden pk fields aren't displayed in the table body and their
4065 corresponding human-readable value is displayed instead. The hidden pk
4066 fields are displayed but separately (not in the table) and only once.
4067 """
4068 story1 = Story.objects.create(
4069 title="The adventures of Guido", content="Once upon a time in Djangoland..."
4070 )
4071 story2 = Story.objects.create(
4072 title="Crouching Tiger, Hidden Python",
4073 content="The Python was sneaking into...",
4074 )
4075 response = self.client.get(reverse("admin:admin_views_story_changelist"))
4076 # Only one hidden field, in a separate place than the table.
4077 self.assertContains(response, 'id="id_form-0-id"', 1)
4078 self.assertContains(response, 'id="id_form-1-id"', 1)
4079 self.assertContains(
4080 response,
4081 '<div class="hiddenfields">\n'
4082 '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
4083 '<input type="hidden" name="form-1-id" value="%d" id="id_form-1-id">\n</div>'
4084 % (story2.id, story1.id),
4085 html=True,
4086 )
4087 self.assertContains(response, '<td class="field-id">%d</td>' % story1.id, 1)
4088 self.assertContains(response, '<td class="field-id">%d</td>' % story2.id, 1)
4089
4090 def test_pk_hidden_fields_with_list_display_links(self):
4091 """ Similarly as test_pk_hidden_fields, but when the hidden pk fields are
4092 referenced in list_display_links.
4093 Refs #12475.
4094 """
4095 story1 = OtherStory.objects.create(
4096 title="The adventures of Guido",
4097 content="Once upon a time in Djangoland...",
4098 )
4099 story2 = OtherStory.objects.create(
4100 title="Crouching Tiger, Hidden Python",
4101 content="The Python was sneaking into...",
4102 )
4103 link1 = reverse("admin:admin_views_otherstory_change", args=(story1.pk,))
4104 link2 = reverse("admin:admin_views_otherstory_change", args=(story2.pk,))
4105 response = self.client.get(reverse("admin:admin_views_otherstory_changelist"))
4106 # Only one hidden field, in a separate place than the table.
4107 self.assertContains(response, 'id="id_form-0-id"', 1)
4108 self.assertContains(response, 'id="id_form-1-id"', 1)
4109 self.assertContains(
4110 response,
4111 '<div class="hiddenfields">\n'
4112 '<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id">'
4113 '<input type="hidden" name="form-1-id" value="%d" id="id_form-1-id">\n</div>'
4114 % (story2.id, story1.id),
4115 html=True,
4116 )
4117 self.assertContains(
4118 response,
4119 '<th class="field-id"><a href="%s">%d</a></th>' % (link1, story1.id),
4120 1,
4121 )
4122 self.assertContains(
4123 response,
4124 '<th class="field-id"><a href="%s">%d</a></th>' % (link2, story2.id),
4125 1,
4126 )
4127
4128
4129@override_settings(ROOT_URLCONF="admin_views.urls")
4130class AdminSearchTest(TestCase):
4131 @classmethod
4132 def setUpTestData(cls):
4133 cls.superuser = User.objects.create_superuser(
4134 username="super", password="secret", email="super@example.com"
4135 )
4136 cls.joepublicuser = User.objects.create_user(
4137 username="joepublic", password="secret"
4138 )
4139 cls.s1 = Section.objects.create(name="Test section")
4140 cls.a1 = Article.objects.create(
4141 content="<p>Middle content</p>",
4142 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
4143 section=cls.s1,
4144 )
4145 cls.a2 = Article.objects.create(
4146 content="<p>Oldest content</p>",
4147 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
4148 section=cls.s1,
4149 )
4150 cls.a3 = Article.objects.create(
4151 content="<p>Newest content</p>",
4152 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
4153 section=cls.s1,
4154 )
4155 cls.p1 = PrePopulatedPost.objects.create(
4156 title="A Long Title", published=True, slug="a-long-title"
4157 )
4158
4159 cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
4160 cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
4161 cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
4162
4163 cls.t1 = Recommender.objects.create()
4164 cls.t2 = Recommendation.objects.create(the_recommender=cls.t1)
4165 cls.t3 = Recommender.objects.create()
4166 cls.t4 = Recommendation.objects.create(the_recommender=cls.t3)
4167
4168 cls.tt1 = TitleTranslation.objects.create(title=cls.t1, text="Bar")
4169 cls.tt2 = TitleTranslation.objects.create(title=cls.t2, text="Foo")
4170 cls.tt3 = TitleTranslation.objects.create(title=cls.t3, text="Few")
4171 cls.tt4 = TitleTranslation.objects.create(title=cls.t4, text="Bas")
4172
4173 def setUp(self):
4174 self.client.force_login(self.superuser)
4175
4176 def test_search_on_sibling_models(self):
4177 "A search that mentions sibling models"
4178 response = self.client.get(
4179 reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4180 )
4181 # confirm the search returned 1 object
4182 self.assertContains(response, "\n1 recommendation\n")
4183
4184 def test_with_fk_to_field(self):
4185 """
4186 The to_field GET parameter is preserved when a search is performed.
4187 Refs #10918.
4188 """
4189 response = self.client.get(
4190 reverse("admin:auth_user_changelist") + "?q=joe&%s=id" % TO_FIELD_VAR
4191 )
4192 self.assertContains(response, "\n1 user\n")
4193 self.assertContains(
4194 response,
4195 '<input type="hidden" name="%s" value="id">' % TO_FIELD_VAR,
4196 html=True,
4197 )
4198
4199 def test_exact_matches(self):
4200 response = self.client.get(
4201 reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4202 )
4203 # confirm the search returned one object
4204 self.assertContains(response, "\n1 recommendation\n")
4205
4206 response = self.client.get(
4207 reverse("admin:admin_views_recommendation_changelist") + "?q=ba"
4208 )
4209 # confirm the search returned zero objects
4210 self.assertContains(response, "\n0 recommendations\n")
4211
4212 def test_beginning_matches(self):
4213 response = self.client.get(
4214 reverse("admin:admin_views_person_changelist") + "?q=Gui"
4215 )
4216 # confirm the search returned one object
4217 self.assertContains(response, "\n1 person\n")
4218 self.assertContains(response, "Guido")
4219
4220 response = self.client.get(
4221 reverse("admin:admin_views_person_changelist") + "?q=uido"
4222 )
4223 # confirm the search returned zero objects
4224 self.assertContains(response, "\n0 persons\n")
4225 self.assertNotContains(response, "Guido")
4226
4227 def test_pluggable_search(self):
4228 PluggableSearchPerson.objects.create(name="Bob", age=10)
4229 PluggableSearchPerson.objects.create(name="Amy", age=20)
4230
4231 response = self.client.get(
4232 reverse("admin:admin_views_pluggablesearchperson_changelist") + "?q=Bob"
4233 )
4234 # confirm the search returned one object
4235 self.assertContains(response, "\n1 pluggable search person\n")
4236 self.assertContains(response, "Bob")
4237
4238 response = self.client.get(
4239 reverse("admin:admin_views_pluggablesearchperson_changelist") + "?q=20"
4240 )
4241 # confirm the search returned one object
4242 self.assertContains(response, "\n1 pluggable search person\n")
4243 self.assertContains(response, "Amy")
4244
4245 def test_reset_link(self):
4246 """
4247 Test presence of reset link in search bar ("1 result (_x total_)").
4248 """
4249 # 1 query for session + 1 for fetching user
4250 # + 1 for filtered result + 1 for filtered count
4251 # + 1 for total count
4252 with self.assertNumQueries(5):
4253 response = self.client.get(
4254 reverse("admin:admin_views_person_changelist") + "?q=Gui"
4255 )
4256 self.assertContains(
4257 response,
4258 """<span class="small quiet">1 result (<a href="?">3 total</a>)</span>""",
4259 html=True,
4260 )
4261
4262 def test_no_total_count(self):
4263 """
4264 #8408 -- "Show all" should be displayed instead of the total count if
4265 ModelAdmin.show_full_result_count is False.
4266 """
4267 # 1 query for session + 1 for fetching user
4268 # + 1 for filtered result + 1 for filtered count
4269 with self.assertNumQueries(4):
4270 response = self.client.get(
4271 reverse("admin:admin_views_recommendation_changelist") + "?q=bar"
4272 )
4273 self.assertContains(
4274 response,
4275 """<span class="small quiet">1 result (<a href="?">Show all</a>)</span>""",
4276 html=True,
4277 )
4278 self.assertTrue(response.context["cl"].show_admin_actions)
4279
4280
4281@override_settings(ROOT_URLCONF="admin_views.urls")
4282class AdminInheritedInlinesTest(TestCase):
4283 @classmethod
4284 def setUpTestData(cls):
4285 cls.superuser = User.objects.create_superuser(
4286 username="super", password="secret", email="super@example.com"
4287 )
4288
4289 def setUp(self):
4290 self.client.force_login(self.superuser)
4291
4292 def test_inline(self):
4293 """
4294 Inline models which inherit from a common parent are correctly handled.
4295 """
4296 foo_user = "foo username"
4297 bar_user = "bar username"
4298
4299 name_re = re.compile(b'name="(.*?)"')
4300
4301 # test the add case
4302 response = self.client.get(reverse("admin:admin_views_persona_add"))
4303 names = name_re.findall(response.content)
4304 # make sure we have no duplicate HTML names
4305 self.assertEqual(len(names), len(set(names)))
4306
4307 # test the add case
4308 post_data = {
4309 "name": "Test Name",
4310 # inline data
4311 "accounts-TOTAL_FORMS": "1",
4312 "accounts-INITIAL_FORMS": "0",
4313 "accounts-MAX_NUM_FORMS": "0",
4314 "accounts-0-username": foo_user,
4315 "accounts-2-TOTAL_FORMS": "1",
4316 "accounts-2-INITIAL_FORMS": "0",
4317 "accounts-2-MAX_NUM_FORMS": "0",
4318 "accounts-2-0-username": bar_user,
4319 }
4320
4321 response = self.client.post(reverse("admin:admin_views_persona_add"), post_data)
4322 self.assertEqual(response.status_code, 302) # redirect somewhere
4323 self.assertEqual(Persona.objects.count(), 1)
4324 self.assertEqual(FooAccount.objects.count(), 1)
4325 self.assertEqual(BarAccount.objects.count(), 1)
4326 self.assertEqual(FooAccount.objects.all()[0].username, foo_user)
4327 self.assertEqual(BarAccount.objects.all()[0].username, bar_user)
4328 self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
4329
4330 persona_id = Persona.objects.all()[0].id
4331 foo_id = FooAccount.objects.all()[0].id
4332 bar_id = BarAccount.objects.all()[0].id
4333
4334 # test the edit case
4335
4336 response = self.client.get(
4337 reverse("admin:admin_views_persona_change", args=(persona_id,))
4338 )
4339 names = name_re.findall(response.content)
4340 # make sure we have no duplicate HTML names
4341 self.assertEqual(len(names), len(set(names)))
4342
4343 post_data = {
4344 "name": "Test Name",
4345 "accounts-TOTAL_FORMS": "2",
4346 "accounts-INITIAL_FORMS": "1",
4347 "accounts-MAX_NUM_FORMS": "0",
4348 "accounts-0-username": "%s-1" % foo_user,
4349 "accounts-0-account_ptr": str(foo_id),
4350 "accounts-0-persona": str(persona_id),
4351 "accounts-2-TOTAL_FORMS": "2",
4352 "accounts-2-INITIAL_FORMS": "1",
4353 "accounts-2-MAX_NUM_FORMS": "0",
4354 "accounts-2-0-username": "%s-1" % bar_user,
4355 "accounts-2-0-account_ptr": str(bar_id),
4356 "accounts-2-0-persona": str(persona_id),
4357 }
4358 response = self.client.post(
4359 reverse("admin:admin_views_persona_change", args=(persona_id,)), post_data
4360 )
4361 self.assertEqual(response.status_code, 302)
4362 self.assertEqual(Persona.objects.count(), 1)
4363 self.assertEqual(FooAccount.objects.count(), 1)
4364 self.assertEqual(BarAccount.objects.count(), 1)
4365 self.assertEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user)
4366 self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user)
4367 self.assertEqual(Persona.objects.all()[0].accounts.count(), 2)
4368
4369
4370@override_settings(ROOT_URLCONF="admin_views.urls")
4371class TestCustomChangeList(TestCase):
4372 @classmethod
4373 def setUpTestData(cls):
4374 cls.superuser = User.objects.create_superuser(
4375 username="super", password="secret", email="super@example.com"
4376 )
4377
4378 def setUp(self):
4379 self.client.force_login(self.superuser)
4380
4381 def test_custom_changelist(self):
4382 """
4383 Validate that a custom ChangeList class can be used (#9749)
4384 """
4385 # Insert some data
4386 post_data = {"name": "First Gadget"}
4387 response = self.client.post(reverse("admin:admin_views_gadget_add"), post_data)
4388 self.assertEqual(response.status_code, 302) # redirect somewhere
4389 # Hit the page once to get messages out of the queue message list
4390 response = self.client.get(reverse("admin:admin_views_gadget_changelist"))
4391 # Data is still not visible on the page
4392 response = self.client.get(reverse("admin:admin_views_gadget_changelist"))
4393 self.assertEqual(response.status_code, 200)
4394 self.assertNotContains(response, "First Gadget")
4395
4396
4397@override_settings(ROOT_URLCONF="admin_views.urls")
4398class TestInlineNotEditable(TestCase):
4399 @classmethod
4400 def setUpTestData(cls):
4401 cls.superuser = User.objects.create_superuser(
4402 username="super", password="secret", email="super@example.com"
4403 )
4404
4405 def setUp(self):
4406 self.client.force_login(self.superuser)
4407
4408 def test_GET_parent_add(self):
4409 """
4410 InlineModelAdmin broken?
4411 """
4412 response = self.client.get(reverse("admin:admin_views_parent_add"))
4413 self.assertEqual(response.status_code, 200)
4414
4415
4416@override_settings(ROOT_URLCONF="admin_views.urls")
4417class AdminCustomQuerysetTest(TestCase):
4418 @classmethod
4419 def setUpTestData(cls):
4420 cls.superuser = User.objects.create_superuser(
4421 username="super", password="secret", email="super@example.com"
4422 )
4423 cls.pks = [EmptyModel.objects.create().id for i in range(3)]
4424
4425 def setUp(self):
4426 self.client.force_login(self.superuser)
4427 self.super_login = {
4428 REDIRECT_FIELD_NAME: reverse("admin:index"),
4429 "username": "super",
4430 "password": "secret",
4431 }
4432
4433 def test_changelist_view(self):
4434 response = self.client.get(reverse("admin:admin_views_emptymodel_changelist"))
4435 for i in self.pks:
4436 if i > 1:
4437 self.assertContains(response, "Primary key = %s" % i)
4438 else:
4439 self.assertNotContains(response, "Primary key = %s" % i)
4440
4441 def test_changelist_view_count_queries(self):
4442 # create 2 Person objects
4443 Person.objects.create(name="person1", gender=1)
4444 Person.objects.create(name="person2", gender=2)
4445 changelist_url = reverse("admin:admin_views_person_changelist")
4446
4447 # 5 queries are expected: 1 for the session, 1 for the user,
4448 # 2 for the counts and 1 for the objects on the page
4449 with self.assertNumQueries(5):
4450 resp = self.client.get(changelist_url)
4451 self.assertEqual(resp.context["selection_note"], "0 of 2 selected")
4452 self.assertEqual(resp.context["selection_note_all"], "All 2 selected")
4453 with self.assertNumQueries(5):
4454 extra = {"q": "not_in_name"}
4455 resp = self.client.get(changelist_url, extra)
4456 self.assertEqual(resp.context["selection_note"], "0 of 0 selected")
4457 self.assertEqual(resp.context["selection_note_all"], "All 0 selected")
4458 with self.assertNumQueries(5):
4459 extra = {"q": "person"}
4460 resp = self.client.get(changelist_url, extra)
4461 self.assertEqual(resp.context["selection_note"], "0 of 2 selected")
4462 self.assertEqual(resp.context["selection_note_all"], "All 2 selected")
4463 with self.assertNumQueries(5):
4464 extra = {"gender__exact": "1"}
4465 resp = self.client.get(changelist_url, extra)
4466 self.assertEqual(resp.context["selection_note"], "0 of 1 selected")
4467 self.assertEqual(resp.context["selection_note_all"], "1 selected")
4468
4469 def test_change_view(self):
4470 for i in self.pks:
4471 url = reverse("admin:admin_views_emptymodel_change", args=(i,))
4472 response = self.client.get(url, follow=True)
4473 if i > 1:
4474 self.assertEqual(response.status_code, 200)
4475 else:
4476 self.assertRedirects(response, reverse("admin:index"))
4477 self.assertEqual(
4478 [m.message for m in response.context["messages"]],
4479 ["empty model with ID “1” doesn’t exist. Perhaps it was deleted?"],
4480 )
4481
4482 def test_add_model_modeladmin_defer_qs(self):
4483 # Test for #14529. defer() is used in ModelAdmin.get_queryset()
4484
4485 # model has __str__ method
4486 self.assertEqual(CoverLetter.objects.count(), 0)
4487 # Emulate model instance creation via the admin
4488 post_data = {
4489 "author": "Candidate, Best",
4490 "_save": "Save",
4491 }
4492 response = self.client.post(
4493 reverse("admin:admin_views_coverletter_add"), post_data, follow=True
4494 )
4495 self.assertEqual(response.status_code, 200)
4496 self.assertEqual(CoverLetter.objects.count(), 1)
4497 # Message should contain non-ugly model verbose name
4498 pk = CoverLetter.objects.all()[0].pk
4499 self.assertContains(
4500 response,
4501 '<li class="success">The cover letter “<a href="%s">'
4502 "Candidate, Best</a>” was added successfully.</li>"
4503 % reverse("admin:admin_views_coverletter_change", args=(pk,)),
4504 html=True,
4505 )
4506
4507 # model has no __str__ method
4508 self.assertEqual(ShortMessage.objects.count(), 0)
4509 # Emulate model instance creation via the admin
4510 post_data = {
4511 "content": "What's this SMS thing?",
4512 "_save": "Save",
4513 }
4514 response = self.client.post(
4515 reverse("admin:admin_views_shortmessage_add"), post_data, follow=True
4516 )
4517 self.assertEqual(response.status_code, 200)
4518 self.assertEqual(ShortMessage.objects.count(), 1)
4519 # Message should contain non-ugly model verbose name
4520 sm = ShortMessage.objects.all()[0]
4521 self.assertContains(
4522 response,
4523 '<li class="success">The short message “<a href="%s">'
4524 "%s</a>” was added successfully.</li>"
4525 % (reverse("admin:admin_views_shortmessage_change", args=(sm.pk,)), sm),
4526 html=True,
4527 )
4528
4529 def test_add_model_modeladmin_only_qs(self):
4530 # Test for #14529. only() is used in ModelAdmin.get_queryset()
4531
4532 # model has __str__ method
4533 self.assertEqual(Telegram.objects.count(), 0)
4534 # Emulate model instance creation via the admin
4535 post_data = {
4536 "title": "Urgent telegram",
4537 "_save": "Save",
4538 }
4539 response = self.client.post(
4540 reverse("admin:admin_views_telegram_add"), post_data, follow=True
4541 )
4542 self.assertEqual(response.status_code, 200)
4543 self.assertEqual(Telegram.objects.count(), 1)
4544 # Message should contain non-ugly model verbose name
4545 pk = Telegram.objects.all()[0].pk
4546 self.assertContains(
4547 response,
4548 '<li class="success">The telegram “<a href="%s">'
4549 "Urgent telegram</a>” was added successfully.</li>"
4550 % reverse("admin:admin_views_telegram_change", args=(pk,)),
4551 html=True,
4552 )
4553
4554 # model has no __str__ method
4555 self.assertEqual(Paper.objects.count(), 0)
4556 # Emulate model instance creation via the admin
4557 post_data = {
4558 "title": "My Modified Paper Title",
4559 "_save": "Save",
4560 }
4561 response = self.client.post(
4562 reverse("admin:admin_views_paper_add"), post_data, follow=True
4563 )
4564 self.assertEqual(response.status_code, 200)
4565 self.assertEqual(Paper.objects.count(), 1)
4566 # Message should contain non-ugly model verbose name
4567 p = Paper.objects.all()[0]
4568 self.assertContains(
4569 response,
4570 '<li class="success">The paper “<a href="%s">'
4571 "%s</a>” was added successfully.</li>"
4572 % (reverse("admin:admin_views_paper_change", args=(p.pk,)), p),
4573 html=True,
4574 )
4575
4576 def test_edit_model_modeladmin_defer_qs(self):
4577 # Test for #14529. defer() is used in ModelAdmin.get_queryset()
4578
4579 # model has __str__ method
4580 cl = CoverLetter.objects.create(author="John Doe")
4581 self.assertEqual(CoverLetter.objects.count(), 1)
4582 response = self.client.get(
4583 reverse("admin:admin_views_coverletter_change", args=(cl.pk,))
4584 )
4585 self.assertEqual(response.status_code, 200)
4586 # Emulate model instance edit via the admin
4587 post_data = {
4588 "author": "John Doe II",
4589 "_save": "Save",
4590 }
4591 url = reverse("admin:admin_views_coverletter_change", args=(cl.pk,))
4592 response = self.client.post(url, post_data, follow=True)
4593 self.assertEqual(response.status_code, 200)
4594 self.assertEqual(CoverLetter.objects.count(), 1)
4595 # Message should contain non-ugly model verbose name. Instance
4596 # representation is set by model's __str__()
4597 self.assertContains(
4598 response,
4599 '<li class="success">The cover letter “<a href="%s">'
4600 "John Doe II</a>” was changed successfully.</li>"
4601 % reverse("admin:admin_views_coverletter_change", args=(cl.pk,)),
4602 html=True,
4603 )
4604
4605 # model has no __str__ method
4606 sm = ShortMessage.objects.create(content="This is expensive")
4607 self.assertEqual(ShortMessage.objects.count(), 1)
4608 response = self.client.get(
4609 reverse("admin:admin_views_shortmessage_change", args=(sm.pk,))
4610 )
4611 self.assertEqual(response.status_code, 200)
4612 # Emulate model instance edit via the admin
4613 post_data = {
4614 "content": "Too expensive",
4615 "_save": "Save",
4616 }
4617 url = reverse("admin:admin_views_shortmessage_change", args=(sm.pk,))
4618 response = self.client.post(url, post_data, follow=True)
4619 self.assertEqual(response.status_code, 200)
4620 self.assertEqual(ShortMessage.objects.count(), 1)
4621 # Message should contain non-ugly model verbose name. The ugly(!)
4622 # instance representation is set by __str__().
4623 self.assertContains(
4624 response,
4625 '<li class="success">The short message “<a href="%s">'
4626 "%s</a>” was changed successfully.</li>"
4627 % (reverse("admin:admin_views_shortmessage_change", args=(sm.pk,)), sm),
4628 html=True,
4629 )
4630
4631 def test_edit_model_modeladmin_only_qs(self):
4632 # Test for #14529. only() is used in ModelAdmin.get_queryset()
4633
4634 # model has __str__ method
4635 t = Telegram.objects.create(title="First Telegram")
4636 self.assertEqual(Telegram.objects.count(), 1)
4637 response = self.client.get(
4638 reverse("admin:admin_views_telegram_change", args=(t.pk,))
4639 )
4640 self.assertEqual(response.status_code, 200)
4641 # Emulate model instance edit via the admin
4642 post_data = {
4643 "title": "Telegram without typo",
4644 "_save": "Save",
4645 }
4646 response = self.client.post(
4647 reverse("admin:admin_views_telegram_change", args=(t.pk,)),
4648 post_data,
4649 follow=True,
4650 )
4651 self.assertEqual(response.status_code, 200)
4652 self.assertEqual(Telegram.objects.count(), 1)
4653 # Message should contain non-ugly model verbose name. The instance
4654 # representation is set by model's __str__()
4655 self.assertContains(
4656 response,
4657 '<li class="success">The telegram “<a href="%s">'
4658 "Telegram without typo</a>” was changed successfully.</li>"
4659 % reverse("admin:admin_views_telegram_change", args=(t.pk,)),
4660 html=True,
4661 )
4662
4663 # model has no __str__ method
4664 p = Paper.objects.create(title="My Paper Title")
4665 self.assertEqual(Paper.objects.count(), 1)
4666 response = self.client.get(
4667 reverse("admin:admin_views_paper_change", args=(p.pk,))
4668 )
4669 self.assertEqual(response.status_code, 200)
4670 # Emulate model instance edit via the admin
4671 post_data = {
4672 "title": "My Modified Paper Title",
4673 "_save": "Save",
4674 }
4675 response = self.client.post(
4676 reverse("admin:admin_views_paper_change", args=(p.pk,)),
4677 post_data,
4678 follow=True,
4679 )
4680 self.assertEqual(response.status_code, 200)
4681 self.assertEqual(Paper.objects.count(), 1)
4682 # Message should contain non-ugly model verbose name. The ugly(!)
4683 # instance representation is set by __str__().
4684 self.assertContains(
4685 response,
4686 '<li class="success">The paper “<a href="%s">'
4687 "%s</a>” was changed successfully.</li>"
4688 % (reverse("admin:admin_views_paper_change", args=(p.pk,)), p),
4689 html=True,
4690 )
4691
4692 def test_history_view_custom_qs(self):
4693 """
4694 Custom querysets are considered for the admin history view.
4695 """
4696 self.client.post(reverse("admin:login"), self.super_login)
4697 FilteredManager.objects.create(pk=1)
4698 FilteredManager.objects.create(pk=2)
4699 response = self.client.get(
4700 reverse("admin:admin_views_filteredmanager_changelist")
4701 )
4702 self.assertContains(response, "PK=1")
4703 self.assertContains(response, "PK=2")
4704 self.assertEqual(
4705 self.client.get(
4706 reverse("admin:admin_views_filteredmanager_history", args=(1,))
4707 ).status_code,
4708 200,
4709 )
4710 self.assertEqual(
4711 self.client.get(
4712 reverse("admin:admin_views_filteredmanager_history", args=(2,))
4713 ).status_code,
4714 200,
4715 )
4716
4717
4718@override_settings(ROOT_URLCONF="admin_views.urls")
4719class AdminInlineFileUploadTest(TestCase):
4720 @classmethod
4721 def setUpTestData(cls):
4722 cls.superuser = User.objects.create_superuser(
4723 username="super", password="secret", email="super@example.com"
4724 )
4725 file1 = tempfile.NamedTemporaryFile(suffix=".file1")
4726 file1.write(b"a" * (2 ** 21))
4727 filename = file1.name
4728 file1.close()
4729 cls.gallery = Gallery.objects.create(name="Test Gallery")
4730 cls.picture = Picture.objects.create(
4731 name="Test Picture", image=filename, gallery=cls.gallery,
4732 )
4733
4734 def setUp(self):
4735 self.client.force_login(self.superuser)
4736
4737 def test_form_has_multipart_enctype(self):
4738 response = self.client.get(
4739 reverse("admin:admin_views_gallery_change", args=(self.gallery.id,))
4740 )
4741 self.assertIs(response.context["has_file_field"], True)
4742 self.assertContains(response, MULTIPART_ENCTYPE)
4743
4744 def test_inline_file_upload_edit_validation_error_post(self):
4745 """
4746 Inline file uploads correctly display prior data (#10002).
4747 """
4748 post_data = {
4749 "name": "Test Gallery",
4750 "pictures-TOTAL_FORMS": "2",
4751 "pictures-INITIAL_FORMS": "1",
4752 "pictures-MAX_NUM_FORMS": "0",
4753 "pictures-0-id": str(self.picture.id),
4754 "pictures-0-gallery": str(self.gallery.id),
4755 "pictures-0-name": "Test Picture",
4756 "pictures-0-image": "",
4757 "pictures-1-id": "",
4758 "pictures-1-gallery": str(self.gallery.id),
4759 "pictures-1-name": "Test Picture 2",
4760 "pictures-1-image": "",
4761 }
4762 response = self.client.post(
4763 reverse("admin:admin_views_gallery_change", args=(self.gallery.id,)),
4764 post_data,
4765 )
4766 self.assertContains(response, b"Currently")
4767
4768
4769@override_settings(ROOT_URLCONF="admin_views.urls")
4770class AdminInlineTests(TestCase):
4771 @classmethod
4772 def setUpTestData(cls):
4773 cls.superuser = User.objects.create_superuser(
4774 username="super", password="secret", email="super@example.com"
4775 )
4776 cls.collector = Collector.objects.create(pk=1, name="John Fowles")
4777
4778 def setUp(self):
4779 self.post_data = {
4780 "name": "Test Name",
4781 "widget_set-TOTAL_FORMS": "3",
4782 "widget_set-INITIAL_FORMS": "0",
4783 "widget_set-MAX_NUM_FORMS": "0",
4784 "widget_set-0-id": "",
4785 "widget_set-0-owner": "1",
4786 "widget_set-0-name": "",
4787 "widget_set-1-id": "",
4788 "widget_set-1-owner": "1",
4789 "widget_set-1-name": "",
4790 "widget_set-2-id": "",
4791 "widget_set-2-owner": "1",
4792 "widget_set-2-name": "",
4793 "doohickey_set-TOTAL_FORMS": "3",
4794 "doohickey_set-INITIAL_FORMS": "0",
4795 "doohickey_set-MAX_NUM_FORMS": "0",
4796 "doohickey_set-0-owner": "1",
4797 "doohickey_set-0-code": "",
4798 "doohickey_set-0-name": "",
4799 "doohickey_set-1-owner": "1",
4800 "doohickey_set-1-code": "",
4801 "doohickey_set-1-name": "",
4802 "doohickey_set-2-owner": "1",
4803 "doohickey_set-2-code": "",
4804 "doohickey_set-2-name": "",
4805 "grommet_set-TOTAL_FORMS": "3",
4806 "grommet_set-INITIAL_FORMS": "0",
4807 "grommet_set-MAX_NUM_FORMS": "0",
4808 "grommet_set-0-code": "",
4809 "grommet_set-0-owner": "1",
4810 "grommet_set-0-name": "",
4811 "grommet_set-1-code": "",
4812 "grommet_set-1-owner": "1",
4813 "grommet_set-1-name": "",
4814 "grommet_set-2-code": "",
4815 "grommet_set-2-owner": "1",
4816 "grommet_set-2-name": "",
4817 "whatsit_set-TOTAL_FORMS": "3",
4818 "whatsit_set-INITIAL_FORMS": "0",
4819 "whatsit_set-MAX_NUM_FORMS": "0",
4820 "whatsit_set-0-owner": "1",
4821 "whatsit_set-0-index": "",
4822 "whatsit_set-0-name": "",
4823 "whatsit_set-1-owner": "1",
4824 "whatsit_set-1-index": "",
4825 "whatsit_set-1-name": "",
4826 "whatsit_set-2-owner": "1",
4827 "whatsit_set-2-index": "",
4828 "whatsit_set-2-name": "",
4829 "fancydoodad_set-TOTAL_FORMS": "3",
4830 "fancydoodad_set-INITIAL_FORMS": "0",
4831 "fancydoodad_set-MAX_NUM_FORMS": "0",
4832 "fancydoodad_set-0-doodad_ptr": "",
4833 "fancydoodad_set-0-owner": "1",
4834 "fancydoodad_set-0-name": "",
4835 "fancydoodad_set-0-expensive": "on",
4836 "fancydoodad_set-1-doodad_ptr": "",
4837 "fancydoodad_set-1-owner": "1",
4838 "fancydoodad_set-1-name": "",
4839 "fancydoodad_set-1-expensive": "on",
4840 "fancydoodad_set-2-doodad_ptr": "",
4841 "fancydoodad_set-2-owner": "1",
4842 "fancydoodad_set-2-name": "",
4843 "fancydoodad_set-2-expensive": "on",
4844 "category_set-TOTAL_FORMS": "3",
4845 "category_set-INITIAL_FORMS": "0",
4846 "category_set-MAX_NUM_FORMS": "0",
4847 "category_set-0-order": "",
4848 "category_set-0-id": "",
4849 "category_set-0-collector": "1",
4850 "category_set-1-order": "",
4851 "category_set-1-id": "",
4852 "category_set-1-collector": "1",
4853 "category_set-2-order": "",
4854 "category_set-2-id": "",
4855 "category_set-2-collector": "1",
4856 }
4857
4858 self.client.force_login(self.superuser)
4859
4860 def test_simple_inline(self):
4861 "A simple model can be saved as inlines"
4862 # First add a new inline
4863 self.post_data["widget_set-0-name"] = "Widget 1"
4864 collector_url = reverse(
4865 "admin:admin_views_collector_change", args=(self.collector.pk,)
4866 )
4867 response = self.client.post(collector_url, self.post_data)
4868 self.assertEqual(response.status_code, 302)
4869 self.assertEqual(Widget.objects.count(), 1)
4870 self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
4871 widget_id = Widget.objects.all()[0].id
4872
4873 # The PK link exists on the rendered form
4874 response = self.client.get(collector_url)
4875 self.assertContains(response, 'name="widget_set-0-id"')
4876
4877 # No file or image fields, no enctype on the forms
4878 self.assertIs(response.context["has_file_field"], False)
4879 self.assertNotContains(response, MULTIPART_ENCTYPE)
4880
4881 # Now resave that inline
4882 self.post_data["widget_set-INITIAL_FORMS"] = "1"
4883 self.post_data["widget_set-0-id"] = str(widget_id)
4884 self.post_data["widget_set-0-name"] = "Widget 1"
4885 response = self.client.post(collector_url, self.post_data)
4886 self.assertEqual(response.status_code, 302)
4887 self.assertEqual(Widget.objects.count(), 1)
4888 self.assertEqual(Widget.objects.all()[0].name, "Widget 1")
4889
4890 # Now modify that inline
4891 self.post_data["widget_set-INITIAL_FORMS"] = "1"
4892 self.post_data["widget_set-0-id"] = str(widget_id)
4893 self.post_data["widget_set-0-name"] = "Widget 1 Updated"
4894 response = self.client.post(collector_url, self.post_data)
4895 self.assertEqual(response.status_code, 302)
4896 self.assertEqual(Widget.objects.count(), 1)
4897 self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated")
4898
4899 def test_explicit_autofield_inline(self):
4900 "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093"
4901 # First add a new inline
4902 self.post_data["grommet_set-0-name"] = "Grommet 1"
4903 collector_url = reverse(
4904 "admin:admin_views_collector_change", args=(self.collector.pk,)
4905 )
4906 response = self.client.post(collector_url, self.post_data)
4907 self.assertEqual(response.status_code, 302)
4908 self.assertEqual(Grommet.objects.count(), 1)
4909 self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
4910
4911 # The PK link exists on the rendered form
4912 response = self.client.get(collector_url)
4913 self.assertContains(response, 'name="grommet_set-0-code"')
4914
4915 # Now resave that inline
4916 self.post_data["grommet_set-INITIAL_FORMS"] = "1"
4917 self.post_data["grommet_set-0-code"] = str(Grommet.objects.all()[0].code)
4918 self.post_data["grommet_set-0-name"] = "Grommet 1"
4919 response = self.client.post(collector_url, self.post_data)
4920 self.assertEqual(response.status_code, 302)
4921 self.assertEqual(Grommet.objects.count(), 1)
4922 self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1")
4923
4924 # Now modify that inline
4925 self.post_data["grommet_set-INITIAL_FORMS"] = "1"
4926 self.post_data["grommet_set-0-code"] = str(Grommet.objects.all()[0].code)
4927 self.post_data["grommet_set-0-name"] = "Grommet 1 Updated"
4928 response = self.client.post(collector_url, self.post_data)
4929 self.assertEqual(response.status_code, 302)
4930 self.assertEqual(Grommet.objects.count(), 1)
4931 self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated")
4932
4933 def test_char_pk_inline(self):
4934 "A model with a character PK can be saved as inlines. Regression for #10992"
4935 # First add a new inline
4936 self.post_data["doohickey_set-0-code"] = "DH1"
4937 self.post_data["doohickey_set-0-name"] = "Doohickey 1"
4938 collector_url = reverse(
4939 "admin:admin_views_collector_change", args=(self.collector.pk,)
4940 )
4941 response = self.client.post(collector_url, self.post_data)
4942 self.assertEqual(response.status_code, 302)
4943 self.assertEqual(DooHickey.objects.count(), 1)
4944 self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
4945
4946 # The PK link exists on the rendered form
4947 response = self.client.get(collector_url)
4948 self.assertContains(response, 'name="doohickey_set-0-code"')
4949
4950 # Now resave that inline
4951 self.post_data["doohickey_set-INITIAL_FORMS"] = "1"
4952 self.post_data["doohickey_set-0-code"] = "DH1"
4953 self.post_data["doohickey_set-0-name"] = "Doohickey 1"
4954 response = self.client.post(collector_url, self.post_data)
4955 self.assertEqual(response.status_code, 302)
4956 self.assertEqual(DooHickey.objects.count(), 1)
4957 self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1")
4958
4959 # Now modify that inline
4960 self.post_data["doohickey_set-INITIAL_FORMS"] = "1"
4961 self.post_data["doohickey_set-0-code"] = "DH1"
4962 self.post_data["doohickey_set-0-name"] = "Doohickey 1 Updated"
4963 response = self.client.post(collector_url, self.post_data)
4964 self.assertEqual(response.status_code, 302)
4965 self.assertEqual(DooHickey.objects.count(), 1)
4966 self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated")
4967
4968 def test_integer_pk_inline(self):
4969 "A model with an integer PK can be saved as inlines. Regression for #10992"
4970 # First add a new inline
4971 self.post_data["whatsit_set-0-index"] = "42"
4972 self.post_data["whatsit_set-0-name"] = "Whatsit 1"
4973 collector_url = reverse(
4974 "admin:admin_views_collector_change", args=(self.collector.pk,)
4975 )
4976 response = self.client.post(collector_url, self.post_data)
4977 self.assertEqual(response.status_code, 302)
4978 self.assertEqual(Whatsit.objects.count(), 1)
4979 self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
4980
4981 # The PK link exists on the rendered form
4982 response = self.client.get(collector_url)
4983 self.assertContains(response, 'name="whatsit_set-0-index"')
4984
4985 # Now resave that inline
4986 self.post_data["whatsit_set-INITIAL_FORMS"] = "1"
4987 self.post_data["whatsit_set-0-index"] = "42"
4988 self.post_data["whatsit_set-0-name"] = "Whatsit 1"
4989 response = self.client.post(collector_url, self.post_data)
4990 self.assertEqual(response.status_code, 302)
4991 self.assertEqual(Whatsit.objects.count(), 1)
4992 self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1")
4993
4994 # Now modify that inline
4995 self.post_data["whatsit_set-INITIAL_FORMS"] = "1"
4996 self.post_data["whatsit_set-0-index"] = "42"
4997 self.post_data["whatsit_set-0-name"] = "Whatsit 1 Updated"
4998 response = self.client.post(collector_url, self.post_data)
4999 self.assertEqual(response.status_code, 302)
5000 self.assertEqual(Whatsit.objects.count(), 1)
5001 self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated")
5002
5003 def test_inherited_inline(self):
5004 "An inherited model can be saved as inlines. Regression for #11042"
5005 # First add a new inline
5006 self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1"
5007 collector_url = reverse(
5008 "admin:admin_views_collector_change", args=(self.collector.pk,)
5009 )
5010 response = self.client.post(collector_url, self.post_data)
5011 self.assertEqual(response.status_code, 302)
5012 self.assertEqual(FancyDoodad.objects.count(), 1)
5013 self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
5014 doodad_pk = FancyDoodad.objects.all()[0].pk
5015
5016 # The PK link exists on the rendered form
5017 response = self.client.get(collector_url)
5018 self.assertContains(response, 'name="fancydoodad_set-0-doodad_ptr"')
5019
5020 # Now resave that inline
5021 self.post_data["fancydoodad_set-INITIAL_FORMS"] = "1"
5022 self.post_data["fancydoodad_set-0-doodad_ptr"] = str(doodad_pk)
5023 self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1"
5024 response = self.client.post(collector_url, self.post_data)
5025 self.assertEqual(response.status_code, 302)
5026 self.assertEqual(FancyDoodad.objects.count(), 1)
5027 self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1")
5028
5029 # Now modify that inline
5030 self.post_data["fancydoodad_set-INITIAL_FORMS"] = "1"
5031 self.post_data["fancydoodad_set-0-doodad_ptr"] = str(doodad_pk)
5032 self.post_data["fancydoodad_set-0-name"] = "Fancy Doodad 1 Updated"
5033 response = self.client.post(collector_url, self.post_data)
5034 self.assertEqual(response.status_code, 302)
5035 self.assertEqual(FancyDoodad.objects.count(), 1)
5036 self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated")
5037
5038 def test_ordered_inline(self):
5039 """
5040 An inline with an editable ordering fields is updated correctly.
5041 """
5042 # Create some objects with an initial ordering
5043 Category.objects.create(id=1, order=1, collector=self.collector)
5044 Category.objects.create(id=2, order=2, collector=self.collector)
5045 Category.objects.create(id=3, order=0, collector=self.collector)
5046 Category.objects.create(id=4, order=0, collector=self.collector)
5047
5048 # NB: The order values must be changed so that the items are reordered.
5049 self.post_data.update(
5050 {
5051 "name": "Frederick Clegg",
5052 "category_set-TOTAL_FORMS": "7",
5053 "category_set-INITIAL_FORMS": "4",
5054 "category_set-MAX_NUM_FORMS": "0",
5055 "category_set-0-order": "14",
5056 "category_set-0-id": "1",
5057 "category_set-0-collector": "1",
5058 "category_set-1-order": "13",
5059 "category_set-1-id": "2",
5060 "category_set-1-collector": "1",
5061 "category_set-2-order": "1",
5062 "category_set-2-id": "3",
5063 "category_set-2-collector": "1",
5064 "category_set-3-order": "0",
5065 "category_set-3-id": "4",
5066 "category_set-3-collector": "1",
5067 "category_set-4-order": "",
5068 "category_set-4-id": "",
5069 "category_set-4-collector": "1",
5070 "category_set-5-order": "",
5071 "category_set-5-id": "",
5072 "category_set-5-collector": "1",
5073 "category_set-6-order": "",
5074 "category_set-6-id": "",
5075 "category_set-6-collector": "1",
5076 }
5077 )
5078 collector_url = reverse(
5079 "admin:admin_views_collector_change", args=(self.collector.pk,)
5080 )
5081 response = self.client.post(collector_url, self.post_data)
5082 # Successful post will redirect
5083 self.assertEqual(response.status_code, 302)
5084
5085 # The order values have been applied to the right objects
5086 self.assertEqual(self.collector.category_set.count(), 4)
5087 self.assertEqual(Category.objects.get(id=1).order, 14)
5088 self.assertEqual(Category.objects.get(id=2).order, 13)
5089 self.assertEqual(Category.objects.get(id=3).order, 1)
5090 self.assertEqual(Category.objects.get(id=4).order, 0)
5091
5092
5093@override_settings(ROOT_URLCONF="admin_views.urls")
5094class NeverCacheTests(TestCase):
5095 @classmethod
5096 def setUpTestData(cls):
5097 cls.superuser = User.objects.create_superuser(
5098 username="super", password="secret", email="super@example.com"
5099 )
5100 cls.s1 = Section.objects.create(name="Test section")
5101
5102 def setUp(self):
5103 self.client.force_login(self.superuser)
5104
5105 def test_admin_index(self):
5106 "Check the never-cache status of the main index"
5107 response = self.client.get(reverse("admin:index"))
5108 self.assertEqual(get_max_age(response), 0)
5109
5110 def test_app_index(self):
5111 "Check the never-cache status of an application index"
5112 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
5113 self.assertEqual(get_max_age(response), 0)
5114
5115 def test_model_index(self):
5116 "Check the never-cache status of a model index"
5117 response = self.client.get(reverse("admin:admin_views_fabric_changelist"))
5118 self.assertEqual(get_max_age(response), 0)
5119
5120 def test_model_add(self):
5121 "Check the never-cache status of a model add page"
5122 response = self.client.get(reverse("admin:admin_views_fabric_add"))
5123 self.assertEqual(get_max_age(response), 0)
5124
5125 def test_model_view(self):
5126 "Check the never-cache status of a model edit page"
5127 response = self.client.get(
5128 reverse("admin:admin_views_section_change", args=(self.s1.pk,))
5129 )
5130 self.assertEqual(get_max_age(response), 0)
5131
5132 def test_model_history(self):
5133 "Check the never-cache status of a model history page"
5134 response = self.client.get(
5135 reverse("admin:admin_views_section_history", args=(self.s1.pk,))
5136 )
5137 self.assertEqual(get_max_age(response), 0)
5138
5139 def test_model_delete(self):
5140 "Check the never-cache status of a model delete page"
5141 response = self.client.get(
5142 reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
5143 )
5144 self.assertEqual(get_max_age(response), 0)
5145
5146 def test_login(self):
5147 "Check the never-cache status of login views"
5148 self.client.logout()
5149 response = self.client.get(reverse("admin:index"))
5150 self.assertEqual(get_max_age(response), 0)
5151
5152 def test_logout(self):
5153 "Check the never-cache status of logout view"
5154 response = self.client.get(reverse("admin:logout"))
5155 self.assertEqual(get_max_age(response), 0)
5156
5157 def test_password_change(self):
5158 "Check the never-cache status of the password change view"
5159 self.client.logout()
5160 response = self.client.get(reverse("admin:password_change"))
5161 self.assertIsNone(get_max_age(response))
5162
5163 def test_password_change_done(self):
5164 "Check the never-cache status of the password change done view"
5165 response = self.client.get(reverse("admin:password_change_done"))
5166 self.assertIsNone(get_max_age(response))
5167
5168 def test_JS_i18n(self):
5169 "Check the never-cache status of the JavaScript i18n view"
5170 response = self.client.get(reverse("admin:jsi18n"))
5171 self.assertIsNone(get_max_age(response))
5172
5173
5174@override_settings(ROOT_URLCONF="admin_views.urls")
5175class PrePopulatedTest(TestCase):
5176 @classmethod
5177 def setUpTestData(cls):
5178 cls.superuser = User.objects.create_superuser(
5179 username="super", password="secret", email="super@example.com"
5180 )
5181 cls.p1 = PrePopulatedPost.objects.create(
5182 title="A Long Title", published=True, slug="a-long-title"
5183 )
5184
5185 def setUp(self):
5186 self.client.force_login(self.superuser)
5187
5188 def test_prepopulated_on(self):
5189 response = self.client.get(reverse("admin:admin_views_prepopulatedpost_add"))
5190 self.assertContains(response, ""id": "#id_slug"")
5191 self.assertContains(
5192 response, ""dependency_ids": ["#id_title"]"
5193 )
5194 self.assertContains(
5195 response,
5196 ""id": "#id_prepopulatedsubpost_set-0-subslug"",
5197 )
5198
5199 def test_prepopulated_off(self):
5200 response = self.client.get(
5201 reverse("admin:admin_views_prepopulatedpost_change", args=(self.p1.pk,))
5202 )
5203 self.assertContains(response, "A Long Title")
5204 self.assertNotContains(response, ""id": "#id_slug"")
5205 self.assertNotContains(
5206 response, ""dependency_ids": ["#id_title"]"
5207 )
5208 self.assertNotContains(
5209 response,
5210 ""id": "#id_prepopulatedsubpost_set-0-subslug"",
5211 )
5212
5213 @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True)
5214 def test_prepopulated_maxlength_localized(self):
5215 """
5216 Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure
5217 that maxLength (in the JavaScript) is rendered without separators.
5218 """
5219 response = self.client.get(
5220 reverse("admin:admin_views_prepopulatedpostlargeslug_add")
5221 )
5222 self.assertContains(response, ""maxLength": 1000") # instead of 1,000
5223
5224 def test_view_only_add_form(self):
5225 """
5226 PrePopulatedPostReadOnlyAdmin.prepopulated_fields includes 'slug'
5227 which is present in the add view, even if the
5228 ModelAdmin.has_change_permission() returns False.
5229 """
5230 response = self.client.get(reverse("admin7:admin_views_prepopulatedpost_add"))
5231 self.assertContains(response, "data-prepopulated-fields=")
5232 self.assertContains(response, ""id": "#id_slug"")
5233
5234 def test_view_only_change_form(self):
5235 """
5236 PrePopulatedPostReadOnlyAdmin.prepopulated_fields includes 'slug'. That
5237 doesn't break a view-only change view.
5238 """
5239 response = self.client.get(
5240 reverse("admin7:admin_views_prepopulatedpost_change", args=(self.p1.pk,))
5241 )
5242 self.assertContains(response, 'data-prepopulated-fields="[]"')
5243 self.assertContains(response, '<div class="readonly">%s</div>' % self.p1.slug)
5244
5245
5246@override_settings(ROOT_URLCONF="admin_views.urls")
5247class SeleniumTests(AdminSeleniumTestCase):
5248
5249 available_apps = ["admin_views"] + AdminSeleniumTestCase.available_apps
5250
5251 def setUp(self):
5252 self.superuser = User.objects.create_superuser(
5253 username="super", password="secret", email="super@example.com"
5254 )
5255 self.p1 = PrePopulatedPost.objects.create(
5256 title="A Long Title", published=True, slug="a-long-title"
5257 )
5258
5259 def test_prepopulated_fields(self):
5260 """
5261 The JavaScript-automated prepopulated fields work with the main form
5262 and with stacked and tabular inlines.
5263 Refs #13068, #9264, #9983, #9784.
5264 """
5265 self.admin_login(
5266 username="super", password="secret", login_url=reverse("admin:index")
5267 )
5268 self.selenium.get(
5269 self.live_server_url + reverse("admin:admin_views_mainprepopulated_add")
5270 )
5271 self.wait_for(".select2")
5272
5273 # Main form ----------------------------------------------------------
5274 self.selenium.find_element_by_id("id_pubdate").send_keys("2012-02-18")
5275 self.select_option("#id_status", "option two")
5276 self.selenium.find_element_by_id("id_name").send_keys(
5277 " this is the mAin nÀMë and it's awεšomeııı"
5278 )
5279 slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5280 slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5281 slug3 = self.selenium.find_element_by_id("id_slug3").get_attribute("value")
5282 self.assertEqual(slug1, "main-name-and-its-awesomeiii-2012-02-18")
5283 self.assertEqual(slug2, "option-two-main-name-and-its-awesomeiii")
5284 self.assertEqual(
5285 slug3,
5286 "this-is-the-main-n\xe0m\xeb-and-its-aw\u03b5\u0161ome\u0131\u0131\u0131",
5287 )
5288
5289 # Stacked inlines ----------------------------------------------------
5290 # Initial inline
5291 self.selenium.find_element_by_id(
5292 "id_relatedprepopulated_set-0-pubdate"
5293 ).send_keys("2011-12-17")
5294 self.select_option("#id_relatedprepopulated_set-0-status", "option one")
5295 self.selenium.find_element_by_id("id_relatedprepopulated_set-0-name").send_keys(
5296 " here is a sŤāÇkeð inline ! "
5297 )
5298 slug1 = self.selenium.find_element_by_id(
5299 "id_relatedprepopulated_set-0-slug1"
5300 ).get_attribute("value")
5301 slug2 = self.selenium.find_element_by_id(
5302 "id_relatedprepopulated_set-0-slug2"
5303 ).get_attribute("value")
5304 self.assertEqual(slug1, "here-stacked-inline-2011-12-17")
5305 self.assertEqual(slug2, "option-one-here-stacked-inline")
5306 initial_select2_inputs = self.selenium.find_elements_by_class_name(
5307 "select2-selection"
5308 )
5309 # Inline formsets have empty/invisible forms.
5310 # Only the 4 visible select2 inputs are initialized.
5311 num_initial_select2_inputs = len(initial_select2_inputs)
5312 self.assertEqual(num_initial_select2_inputs, 4)
5313
5314 # Add an inline
5315 self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5316 0
5317 ].click()
5318 self.assertEqual(
5319 len(self.selenium.find_elements_by_class_name("select2-selection")),
5320 num_initial_select2_inputs + 2,
5321 )
5322 self.selenium.find_element_by_id(
5323 "id_relatedprepopulated_set-1-pubdate"
5324 ).send_keys("1999-01-25")
5325 self.select_option("#id_relatedprepopulated_set-1-status", "option two")
5326 self.selenium.find_element_by_id("id_relatedprepopulated_set-1-name").send_keys(
5327 " now you haVe anöther sŤāÇkeð inline with a very ... "
5328 "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... "
5329 )
5330 slug1 = self.selenium.find_element_by_id(
5331 "id_relatedprepopulated_set-1-slug1"
5332 ).get_attribute("value")
5333 slug2 = self.selenium.find_element_by_id(
5334 "id_relatedprepopulated_set-1-slug2"
5335 ).get_attribute("value")
5336 # 50 characters maximum for slug1 field
5337 self.assertEqual(slug1, "now-you-have-another-stacked-inline-very-loooooooo")
5338 # 60 characters maximum for slug2 field
5339 self.assertEqual(
5340 slug2, "option-two-now-you-have-another-stacked-inline-very-looooooo"
5341 )
5342
5343 # Tabular inlines ----------------------------------------------------
5344 # Initial inline
5345 self.selenium.find_element_by_id(
5346 "id_relatedprepopulated_set-2-0-pubdate"
5347 ).send_keys("1234-12-07")
5348 self.select_option("#id_relatedprepopulated_set-2-0-status", "option two")
5349 self.selenium.find_element_by_id(
5350 "id_relatedprepopulated_set-2-0-name"
5351 ).send_keys("And now, with a tÃbűlaŘ inline !!!")
5352 slug1 = self.selenium.find_element_by_id(
5353 "id_relatedprepopulated_set-2-0-slug1"
5354 ).get_attribute("value")
5355 slug2 = self.selenium.find_element_by_id(
5356 "id_relatedprepopulated_set-2-0-slug2"
5357 ).get_attribute("value")
5358 self.assertEqual(slug1, "and-now-tabular-inline-1234-12-07")
5359 self.assertEqual(slug2, "option-two-and-now-tabular-inline")
5360
5361 # Add an inline
5362 self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5363 1
5364 ].click()
5365 self.assertEqual(
5366 len(self.selenium.find_elements_by_class_name("select2-selection")),
5367 num_initial_select2_inputs + 4,
5368 )
5369 self.selenium.find_element_by_id(
5370 "id_relatedprepopulated_set-2-1-pubdate"
5371 ).send_keys("1981-08-22")
5372 self.select_option("#id_relatedprepopulated_set-2-1-status", "option one")
5373 self.selenium.find_element_by_id(
5374 "id_relatedprepopulated_set-2-1-name"
5375 ).send_keys(r'a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters')
5376 slug1 = self.selenium.find_element_by_id(
5377 "id_relatedprepopulated_set-2-1-slug1"
5378 ).get_attribute("value")
5379 slug2 = self.selenium.find_element_by_id(
5380 "id_relatedprepopulated_set-2-1-slug2"
5381 ).get_attribute("value")
5382 self.assertEqual(slug1, "tabular-inline-ignored-characters-1981-08-22")
5383 self.assertEqual(slug2, "option-one-tabular-inline-ignored-characters")
5384 # Add an inline without an initial inline.
5385 # The button is outside of the browser frame.
5386 self.selenium.execute_script("window.scrollTo(0, document.body.scrollHeight);")
5387 self.selenium.find_elements_by_link_text("Add another Related prepopulated")[
5388 2
5389 ].click()
5390 self.assertEqual(
5391 len(self.selenium.find_elements_by_class_name("select2-selection")),
5392 num_initial_select2_inputs + 6,
5393 )
5394 # Save and check that everything is properly stored in the database
5395 with self.wait_page_loaded():
5396 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5397 self.assertEqual(MainPrepopulated.objects.all().count(), 1)
5398 MainPrepopulated.objects.get(
5399 name=" this is the mAin nÀMë and it's awεšomeııı",
5400 pubdate="2012-02-18",
5401 status="option two",
5402 slug1="main-name-and-its-awesomeiii-2012-02-18",
5403 slug2="option-two-main-name-and-its-awesomeiii",
5404 )
5405 self.assertEqual(RelatedPrepopulated.objects.all().count(), 4)
5406 RelatedPrepopulated.objects.get(
5407 name=" here is a sŤāÇkeð inline ! ",
5408 pubdate="2011-12-17",
5409 status="option one",
5410 slug1="here-stacked-inline-2011-12-17",
5411 slug2="option-one-here-stacked-inline",
5412 )
5413 RelatedPrepopulated.objects.get(
5414 # 75 characters in name field
5415 name=" now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo",
5416 pubdate="1999-01-25",
5417 status="option two",
5418 slug1="now-you-have-another-stacked-inline-very-loooooooo",
5419 slug2="option-two-now-you-have-another-stacked-inline-very-looooooo",
5420 )
5421 RelatedPrepopulated.objects.get(
5422 name="And now, with a tÃbűlaŘ inline !!!",
5423 pubdate="1234-12-07",
5424 status="option two",
5425 slug1="and-now-tabular-inline-1234-12-07",
5426 slug2="option-two-and-now-tabular-inline",
5427 )
5428 RelatedPrepopulated.objects.get(
5429 name=r'a tÃbűlaŘ inline with ignored ;"&*^\%$#@-/`~ characters',
5430 pubdate="1981-08-22",
5431 status="option one",
5432 slug1="tabular-inline-ignored-characters-1981-08-22",
5433 slug2="option-one-tabular-inline-ignored-characters",
5434 )
5435
5436 def test_populate_existing_object(self):
5437 """
5438 The prepopulation works for existing objects too, as long as
5439 the original field is empty (#19082).
5440 """
5441 # Slugs are empty to start with.
5442 item = MainPrepopulated.objects.create(
5443 name=" this is the mAin nÀMë",
5444 pubdate="2012-02-18",
5445 status="option two",
5446 slug1="",
5447 slug2="",
5448 )
5449 self.admin_login(
5450 username="super", password="secret", login_url=reverse("admin:index")
5451 )
5452
5453 object_url = self.live_server_url + reverse(
5454 "admin:admin_views_mainprepopulated_change", args=(item.id,)
5455 )
5456
5457 self.selenium.get(object_url)
5458 self.selenium.find_element_by_id("id_name").send_keys(" the best")
5459
5460 # The slugs got prepopulated since they were originally empty
5461 slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5462 slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5463 self.assertEqual(slug1, "main-name-best-2012-02-18")
5464 self.assertEqual(slug2, "option-two-main-name-best")
5465
5466 # Save the object
5467 with self.wait_page_loaded():
5468 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5469
5470 self.selenium.get(object_url)
5471 self.selenium.find_element_by_id("id_name").send_keys(" hello")
5472
5473 # The slugs got prepopulated didn't change since they were originally not empty
5474 slug1 = self.selenium.find_element_by_id("id_slug1").get_attribute("value")
5475 slug2 = self.selenium.find_element_by_id("id_slug2").get_attribute("value")
5476 self.assertEqual(slug1, "main-name-best-2012-02-18")
5477 self.assertEqual(slug2, "option-two-main-name-best")
5478
5479 def test_collapsible_fieldset(self):
5480 """
5481 The 'collapse' class in fieldsets definition allows to
5482 show/hide the appropriate field section.
5483 """
5484 self.admin_login(
5485 username="super", password="secret", login_url=reverse("admin:index")
5486 )
5487 self.selenium.get(
5488 self.live_server_url + reverse("admin:admin_views_article_add")
5489 )
5490 self.assertFalse(self.selenium.find_element_by_id("id_title").is_displayed())
5491 self.selenium.find_elements_by_link_text("Show")[0].click()
5492 self.assertTrue(self.selenium.find_element_by_id("id_title").is_displayed())
5493 self.assertEqual(
5494 self.selenium.find_element_by_id("fieldsetcollapser0").text, "Hide"
5495 )
5496
5497 def test_first_field_focus(self):
5498 """JavaScript-assisted auto-focus on first usable form field."""
5499 # First form field has a single widget
5500 self.admin_login(
5501 username="super", password="secret", login_url=reverse("admin:index")
5502 )
5503 with self.wait_page_loaded():
5504 self.selenium.get(
5505 self.live_server_url + reverse("admin:admin_views_picture_add")
5506 )
5507 self.assertEqual(
5508 self.selenium.switch_to.active_element,
5509 self.selenium.find_element_by_id("id_name"),
5510 )
5511
5512 # First form field has a MultiWidget
5513 with self.wait_page_loaded():
5514 self.selenium.get(
5515 self.live_server_url + reverse("admin:admin_views_reservation_add")
5516 )
5517 self.assertEqual(
5518 self.selenium.switch_to.active_element,
5519 self.selenium.find_element_by_id("id_start_date_0"),
5520 )
5521
5522 def test_cancel_delete_confirmation(self):
5523 "Cancelling the deletion of an object takes the user back one page."
5524 pizza = Pizza.objects.create(name="Double Cheese")
5525 url = reverse("admin:admin_views_pizza_change", args=(pizza.id,))
5526 full_url = self.live_server_url + url
5527 self.admin_login(
5528 username="super", password="secret", login_url=reverse("admin:index")
5529 )
5530 self.selenium.get(full_url)
5531 self.selenium.find_element_by_class_name("deletelink").click()
5532 # Click 'cancel' on the delete page.
5533 self.selenium.find_element_by_class_name("cancel-link").click()
5534 # Wait until we're back on the change page.
5535 self.wait_for_text("#content h1", "Change pizza")
5536 self.assertEqual(self.selenium.current_url, full_url)
5537 self.assertEqual(Pizza.objects.count(), 1)
5538
5539 def test_cancel_delete_related_confirmation(self):
5540 """
5541 Cancelling the deletion of an object with relations takes the user back
5542 one page.
5543 """
5544 pizza = Pizza.objects.create(name="Double Cheese")
5545 topping1 = Topping.objects.create(name="Cheddar")
5546 topping2 = Topping.objects.create(name="Mozzarella")
5547 pizza.toppings.add(topping1, topping2)
5548 url = reverse("admin:admin_views_pizza_change", args=(pizza.id,))
5549 full_url = self.live_server_url + url
5550 self.admin_login(
5551 username="super", password="secret", login_url=reverse("admin:index")
5552 )
5553 self.selenium.get(full_url)
5554 self.selenium.find_element_by_class_name("deletelink").click()
5555 # Click 'cancel' on the delete page.
5556 self.selenium.find_element_by_class_name("cancel-link").click()
5557 # Wait until we're back on the change page.
5558 self.wait_for_text("#content h1", "Change pizza")
5559 self.assertEqual(self.selenium.current_url, full_url)
5560 self.assertEqual(Pizza.objects.count(), 1)
5561 self.assertEqual(Topping.objects.count(), 2)
5562
5563 def test_list_editable_popups(self):
5564 """
5565 list_editable foreign keys have add/change popups.
5566 """
5567 from selenium.webdriver.support.ui import Select
5568
5569 s1 = Section.objects.create(name="Test section")
5570 Article.objects.create(
5571 title="foo",
5572 content="<p>Middle content</p>",
5573 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
5574 section=s1,
5575 )
5576 self.admin_login(
5577 username="super", password="secret", login_url=reverse("admin:index")
5578 )
5579 self.selenium.get(
5580 self.live_server_url + reverse("admin:admin_views_article_changelist")
5581 )
5582 # Change popup
5583 self.selenium.find_element_by_id("change_id_form-0-section").click()
5584 self.wait_for_and_switch_to_popup()
5585 self.wait_for_text("#content h1", "Change section")
5586 name_input = self.selenium.find_element_by_id("id_name")
5587 name_input.clear()
5588 name_input.send_keys("<i>edited section</i>")
5589 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5590 self.selenium.switch_to.window(self.selenium.window_handles[0])
5591 select = Select(self.selenium.find_element_by_id("id_form-0-section"))
5592 self.assertEqual(select.first_selected_option.text, "<i>edited section</i>")
5593 # Rendered select2 input.
5594 select2_display = self.selenium.find_element_by_class_name(
5595 "select2-selection__rendered"
5596 )
5597 # Clear button (×\n) is included in text.
5598 self.assertEqual(select2_display.text, "×\n<i>edited section</i>")
5599
5600 # Add popup
5601 self.selenium.find_element_by_id("add_id_form-0-section").click()
5602 self.wait_for_and_switch_to_popup()
5603 self.wait_for_text("#content h1", "Add section")
5604 self.selenium.find_element_by_id("id_name").send_keys("new section")
5605 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5606 self.selenium.switch_to.window(self.selenium.window_handles[0])
5607 select = Select(self.selenium.find_element_by_id("id_form-0-section"))
5608 self.assertEqual(select.first_selected_option.text, "new section")
5609 select2_display = self.selenium.find_element_by_class_name(
5610 "select2-selection__rendered"
5611 )
5612 # Clear button (×\n) is included in text.
5613 self.assertEqual(select2_display.text, "×\nnew section")
5614
5615 def test_inline_uuid_pk_edit_with_popup(self):
5616 from selenium.webdriver.support.ui import Select
5617
5618 parent = ParentWithUUIDPK.objects.create(title="test")
5619 related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5620 self.admin_login(
5621 username="super", password="secret", login_url=reverse("admin:index")
5622 )
5623 change_url = reverse(
5624 "admin:admin_views_relatedwithuuidpkmodel_change",
5625 args=(related_with_parent.id,),
5626 )
5627 self.selenium.get(self.live_server_url + change_url)
5628 self.selenium.find_element_by_id("change_id_parent").click()
5629 self.wait_for_and_switch_to_popup()
5630 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5631 self.selenium.switch_to.window(self.selenium.window_handles[0])
5632 select = Select(self.selenium.find_element_by_id("id_parent"))
5633 self.assertEqual(select.first_selected_option.text, str(parent.id))
5634 self.assertEqual(
5635 select.first_selected_option.get_attribute("value"), str(parent.id)
5636 )
5637
5638 def test_inline_uuid_pk_add_with_popup(self):
5639 from selenium.webdriver.support.ui import Select
5640
5641 self.admin_login(
5642 username="super", password="secret", login_url=reverse("admin:index")
5643 )
5644 self.selenium.get(
5645 self.live_server_url
5646 + reverse("admin:admin_views_relatedwithuuidpkmodel_add")
5647 )
5648 self.selenium.find_element_by_id("add_id_parent").click()
5649 self.wait_for_and_switch_to_popup()
5650 self.selenium.find_element_by_id("id_title").send_keys("test")
5651 self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
5652 self.selenium.switch_to.window(self.selenium.window_handles[0])
5653 select = Select(self.selenium.find_element_by_id("id_parent"))
5654 uuid_id = str(ParentWithUUIDPK.objects.first().id)
5655 self.assertEqual(select.first_selected_option.text, uuid_id)
5656 self.assertEqual(select.first_selected_option.get_attribute("value"), uuid_id)
5657
5658 def test_inline_uuid_pk_delete_with_popup(self):
5659 from selenium.webdriver.support.ui import Select
5660
5661 parent = ParentWithUUIDPK.objects.create(title="test")
5662 related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5663 self.admin_login(
5664 username="super", password="secret", login_url=reverse("admin:index")
5665 )
5666 change_url = reverse(
5667 "admin:admin_views_relatedwithuuidpkmodel_change",
5668 args=(related_with_parent.id,),
5669 )
5670 self.selenium.get(self.live_server_url + change_url)
5671 self.selenium.find_element_by_id("delete_id_parent").click()
5672 self.wait_for_and_switch_to_popup()
5673 self.selenium.find_element_by_xpath('//input[@value="Yes, I’m sure"]').click()
5674 self.selenium.switch_to.window(self.selenium.window_handles[0])
5675 select = Select(self.selenium.find_element_by_id("id_parent"))
5676 self.assertEqual(ParentWithUUIDPK.objects.count(), 0)
5677 self.assertEqual(select.first_selected_option.text, "---------")
5678 self.assertEqual(select.first_selected_option.get_attribute("value"), "")
5679
5680 def test_inline_with_popup_cancel_delete(self):
5681 """Clicking ""No, take me back" on a delete popup closes the window."""
5682 parent = ParentWithUUIDPK.objects.create(title="test")
5683 related_with_parent = RelatedWithUUIDPKModel.objects.create(parent=parent)
5684 self.admin_login(
5685 username="super", password="secret", login_url=reverse("admin:index")
5686 )
5687 change_url = reverse(
5688 "admin:admin_views_relatedwithuuidpkmodel_change",
5689 args=(related_with_parent.id,),
5690 )
5691 self.selenium.get(self.live_server_url + change_url)
5692 self.selenium.find_element_by_id("delete_id_parent").click()
5693 self.wait_for_and_switch_to_popup()
5694 self.selenium.find_element_by_xpath('//a[text()="No, take me back"]').click()
5695 self.selenium.switch_to.window(self.selenium.window_handles[0])
5696 self.assertEqual(len(self.selenium.window_handles), 1)
5697
5698 def test_list_editable_raw_id_fields(self):
5699 parent = ParentWithUUIDPK.objects.create(title="test")
5700 parent2 = ParentWithUUIDPK.objects.create(title="test2")
5701 RelatedWithUUIDPKModel.objects.create(parent=parent)
5702 self.admin_login(
5703 username="super", password="secret", login_url=reverse("admin:index")
5704 )
5705 change_url = reverse(
5706 "admin:admin_views_relatedwithuuidpkmodel_changelist",
5707 current_app=site2.name,
5708 )
5709 self.selenium.get(self.live_server_url + change_url)
5710 self.selenium.find_element_by_id("lookup_id_form-0-parent").click()
5711 self.wait_for_and_switch_to_popup()
5712 # Select "parent2" in the popup.
5713 self.selenium.find_element_by_link_text(str(parent2.pk)).click()
5714 self.selenium.switch_to.window(self.selenium.window_handles[0])
5715 # The newly selected pk should appear in the raw id input.
5716 value = self.selenium.find_element_by_id("id_form-0-parent").get_attribute(
5717 "value"
5718 )
5719 self.assertEqual(value, str(parent2.pk))
5720
5721
5722@override_settings(ROOT_URLCONF="admin_views.urls")
5723class ReadonlyTest(AdminFieldExtractionMixin, TestCase):
5724 @classmethod
5725 def setUpTestData(cls):
5726 cls.superuser = User.objects.create_superuser(
5727 username="super", password="secret", email="super@example.com"
5728 )
5729
5730 def setUp(self):
5731 self.client.force_login(self.superuser)
5732
5733 def test_readonly_get(self):
5734 response = self.client.get(reverse("admin:admin_views_post_add"))
5735 self.assertEqual(response.status_code, 200)
5736 self.assertNotContains(response, 'name="posted"')
5737 # 3 fields + 2 submit buttons + 5 inline management form fields, + 2
5738 # hidden fields for inlines + 1 field for the inline + 2 empty form
5739 self.assertContains(response, "<input", count=15)
5740 self.assertContains(response, formats.localize(datetime.date.today()))
5741 self.assertContains(response, "<label>Awesomeness level:</label>")
5742 self.assertContains(response, "Very awesome.")
5743 self.assertContains(response, "Unknown coolness.")
5744 self.assertContains(response, "foo")
5745
5746 # Multiline text in a readonly field gets <br> tags
5747 self.assertContains(response, "Multiline<br>test<br>string")
5748 self.assertContains(
5749 response,
5750 '<div class="readonly">Multiline<br>html<br>content</div>',
5751 html=True,
5752 )
5753 self.assertContains(response, "InlineMultiline<br>test<br>string")
5754
5755 self.assertContains(
5756 response,
5757 formats.localize(datetime.date.today() - datetime.timedelta(days=7)),
5758 )
5759
5760 self.assertContains(response, '<div class="form-row field-coolness">')
5761 self.assertContains(response, '<div class="form-row field-awesomeness_level">')
5762 self.assertContains(response, '<div class="form-row field-posted">')
5763 self.assertContains(response, '<div class="form-row field-value">')
5764 self.assertContains(response, '<div class="form-row">')
5765 self.assertContains(response, '<div class="help">', 3)
5766 self.assertContains(
5767 response,
5768 '<div class="help">Some help text for the title (with unicode ŠĐĆŽćžšđ)</div>',
5769 html=True,
5770 )
5771 self.assertContains(
5772 response,
5773 '<div class="help">Some help text for the content (with unicode ŠĐĆŽćžšđ)</div>',
5774 html=True,
5775 )
5776 self.assertContains(
5777 response,
5778 '<div class="help">Some help text for the date (with unicode ŠĐĆŽćžšđ)</div>',
5779 html=True,
5780 )
5781
5782 p = Post.objects.create(
5783 title="I worked on readonly_fields", content="Its good stuff"
5784 )
5785 response = self.client.get(
5786 reverse("admin:admin_views_post_change", args=(p.pk,))
5787 )
5788 self.assertContains(response, "%d amount of cool" % p.pk)
5789
5790 def test_readonly_text_field(self):
5791 p = Post.objects.create(
5792 title="Readonly test",
5793 content="test",
5794 readonly_content="test\r\n\r\ntest\r\n\r\ntest\r\n\r\ntest",
5795 )
5796 Link.objects.create(
5797 url="http://www.djangoproject.com",
5798 post=p,
5799 readonly_link_content="test\r\nlink",
5800 )
5801 response = self.client.get(
5802 reverse("admin:admin_views_post_change", args=(p.pk,))
5803 )
5804 # Checking readonly field.
5805 self.assertContains(response, "test<br><br>test<br><br>test<br><br>test")
5806 # Checking readonly field in inline.
5807 self.assertContains(response, "test<br>link")
5808
5809 def test_readonly_post(self):
5810 data = {
5811 "title": "Django Got Readonly Fields",
5812 "content": "This is an incredible development.",
5813 "link_set-TOTAL_FORMS": "1",
5814 "link_set-INITIAL_FORMS": "0",
5815 "link_set-MAX_NUM_FORMS": "0",
5816 }
5817 response = self.client.post(reverse("admin:admin_views_post_add"), data)
5818 self.assertEqual(response.status_code, 302)
5819 self.assertEqual(Post.objects.count(), 1)
5820 p = Post.objects.get()
5821 self.assertEqual(p.posted, datetime.date.today())
5822
5823 data["posted"] = "10-8-1990" # some date that's not today
5824 response = self.client.post(reverse("admin:admin_views_post_add"), data)
5825 self.assertEqual(response.status_code, 302)
5826 self.assertEqual(Post.objects.count(), 2)
5827 p = Post.objects.order_by("-id")[0]
5828 self.assertEqual(p.posted, datetime.date.today())
5829
5830 def test_readonly_manytomany(self):
5831 "Regression test for #13004"
5832 response = self.client.get(reverse("admin:admin_views_pizza_add"))
5833 self.assertEqual(response.status_code, 200)
5834
5835 def test_user_password_change_limited_queryset(self):
5836 su = User.objects.filter(is_superuser=True)[0]
5837 response = self.client.get(
5838 reverse("admin2:auth_user_password_change", args=(su.pk,))
5839 )
5840 self.assertEqual(response.status_code, 404)
5841
5842 def test_change_form_renders_correct_null_choice_value(self):
5843 """
5844 Regression test for #17911.
5845 """
5846 choice = Choice.objects.create(choice=None)
5847 response = self.client.get(
5848 reverse("admin:admin_views_choice_change", args=(choice.pk,))
5849 )
5850 self.assertContains(
5851 response, '<div class="readonly">No opinion</div>', html=True
5852 )
5853
5854 def test_readonly_manytomany_backwards_ref(self):
5855 """
5856 Regression test for #16433 - backwards references for related objects
5857 broke if the related field is read-only due to the help_text attribute
5858 """
5859 topping = Topping.objects.create(name="Salami")
5860 pizza = Pizza.objects.create(name="Americano")
5861 pizza.toppings.add(topping)
5862 response = self.client.get(reverse("admin:admin_views_topping_add"))
5863 self.assertEqual(response.status_code, 200)
5864
5865 def test_readonly_manytomany_forwards_ref(self):
5866 topping = Topping.objects.create(name="Salami")
5867 pizza = Pizza.objects.create(name="Americano")
5868 pizza.toppings.add(topping)
5869 response = self.client.get(
5870 reverse("admin:admin_views_pizza_change", args=(pizza.pk,))
5871 )
5872 self.assertContains(response, "<label>Toppings:</label>", html=True)
5873 self.assertContains(response, '<div class="readonly">Salami</div>', html=True)
5874
5875 def test_readonly_onetoone_backwards_ref(self):
5876 """
5877 Can reference a reverse OneToOneField in ModelAdmin.readonly_fields.
5878 """
5879 v1 = Villain.objects.create(name="Adam")
5880 pl = Plot.objects.create(name="Test Plot", team_leader=v1, contact=v1)
5881 pd = PlotDetails.objects.create(details="Brand New Plot", plot=pl)
5882
5883 response = self.client.get(
5884 reverse("admin:admin_views_plotproxy_change", args=(pl.pk,))
5885 )
5886 field = self.get_admin_readonly_field(response, "plotdetails")
5887 self.assertEqual(field.contents(), "Brand New Plot")
5888
5889 # The reverse relation also works if the OneToOneField is null.
5890 pd.plot = None
5891 pd.save()
5892
5893 response = self.client.get(
5894 reverse("admin:admin_views_plotproxy_change", args=(pl.pk,))
5895 )
5896 field = self.get_admin_readonly_field(response, "plotdetails")
5897 self.assertEqual(field.contents(), "-") # default empty value
5898
5899 def test_readonly_field_overrides(self):
5900 """
5901 Regression test for #22087 - ModelForm Meta overrides are ignored by
5902 AdminReadonlyField
5903 """
5904 p = FieldOverridePost.objects.create(title="Test Post", content="Test Content")
5905 response = self.client.get(
5906 reverse("admin:admin_views_fieldoverridepost_change", args=(p.pk,))
5907 )
5908 self.assertContains(
5909 response, '<div class="help">Overridden help text for the date</div>'
5910 )
5911 self.assertContains(
5912 response,
5913 '<label for="id_public">Overridden public label:</label>',
5914 html=True,
5915 )
5916 self.assertNotContains(
5917 response, "Some help text for the date (with unicode ŠĐĆŽćžšđ)"
5918 )
5919
5920 def test_correct_autoescaping(self):
5921 """
5922 Make sure that non-field readonly elements are properly autoescaped (#24461)
5923 """
5924 section = Section.objects.create(name="<a>evil</a>")
5925 response = self.client.get(
5926 reverse("admin:admin_views_section_change", args=(section.pk,))
5927 )
5928 self.assertNotContains(response, "<a>evil</a>", status_code=200)
5929 self.assertContains(response, "<a>evil</a>", status_code=200)
5930
5931 def test_label_suffix_translated(self):
5932 pizza = Pizza.objects.create(name="Americano")
5933 url = reverse("admin:admin_views_pizza_change", args=(pizza.pk,))
5934 with self.settings(LANGUAGE_CODE="fr"):
5935 response = self.client.get(url)
5936 self.assertContains(response, "<label>Toppings\u00A0:</label>", html=True)
5937
5938
5939@override_settings(ROOT_URLCONF="admin_views.urls")
5940class LimitChoicesToInAdminTest(TestCase):
5941 @classmethod
5942 def setUpTestData(cls):
5943 cls.superuser = User.objects.create_superuser(
5944 username="super", password="secret", email="super@example.com"
5945 )
5946
5947 def setUp(self):
5948 self.client.force_login(self.superuser)
5949
5950 def test_limit_choices_to_as_callable(self):
5951 """Test for ticket 2445 changes to admin."""
5952 threepwood = Character.objects.create(
5953 username="threepwood",
5954 last_action=datetime.datetime.today() + datetime.timedelta(days=1),
5955 )
5956 marley = Character.objects.create(
5957 username="marley",
5958 last_action=datetime.datetime.today() - datetime.timedelta(days=1),
5959 )
5960 response = self.client.get(reverse("admin:admin_views_stumpjoke_add"))
5961 # The allowed option should appear twice; the limited option should not appear.
5962 self.assertContains(response, threepwood.username, count=2)
5963 self.assertNotContains(response, marley.username)
5964
5965
5966@override_settings(ROOT_URLCONF="admin_views.urls")
5967class RawIdFieldsTest(TestCase):
5968 @classmethod
5969 def setUpTestData(cls):
5970 cls.superuser = User.objects.create_superuser(
5971 username="super", password="secret", email="super@example.com"
5972 )
5973
5974 def setUp(self):
5975 self.client.force_login(self.superuser)
5976
5977 def test_limit_choices_to(self):
5978 """Regression test for 14880"""
5979 actor = Actor.objects.create(name="Palin", age=27)
5980 Inquisition.objects.create(expected=True, leader=actor, country="England")
5981 Inquisition.objects.create(expected=False, leader=actor, country="Spain")
5982 response = self.client.get(reverse("admin:admin_views_sketch_add"))
5983 # Find the link
5984 m = re.search(
5985 br'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content
5986 )
5987 self.assertTrue(m) # Got a match
5988 popup_url = m.groups()[0].decode().replace("&", "&")
5989
5990 # Handle relative links
5991 popup_url = urljoin(response.request["PATH_INFO"], popup_url)
5992 # Get the popup and verify the correct objects show up in the resulting
5993 # page. This step also tests integers, strings and booleans in the
5994 # lookup query string; in model we define inquisition field to have a
5995 # limit_choices_to option that includes a filter on a string field
5996 # (inquisition__actor__name), a filter on an integer field
5997 # (inquisition__actor__age), and a filter on a boolean field
5998 # (inquisition__expected).
5999 response2 = self.client.get(popup_url)
6000 self.assertContains(response2, "Spain")
6001 self.assertNotContains(response2, "England")
6002
6003 def test_limit_choices_to_isnull_false(self):
6004 """Regression test for 20182"""
6005 Actor.objects.create(name="Palin", age=27)
6006 Actor.objects.create(name="Kilbraken", age=50, title="Judge")
6007 response = self.client.get(reverse("admin:admin_views_sketch_add"))
6008 # Find the link
6009 m = re.search(
6010 br'<a href="([^"]*)"[^>]* id="lookup_id_defendant0"', response.content
6011 )
6012 self.assertTrue(m) # Got a match
6013 popup_url = m.groups()[0].decode().replace("&", "&")
6014
6015 # Handle relative links
6016 popup_url = urljoin(response.request["PATH_INFO"], popup_url)
6017 # Get the popup and verify the correct objects show up in the resulting
6018 # page. This step tests field__isnull=0 gets parsed correctly from the
6019 # lookup query string; in model we define defendant0 field to have a
6020 # limit_choices_to option that includes "actor__title__isnull=False".
6021 response2 = self.client.get(popup_url)
6022 self.assertContains(response2, "Kilbraken")
6023 self.assertNotContains(response2, "Palin")
6024
6025 def test_limit_choices_to_isnull_true(self):
6026 """Regression test for 20182"""
6027 Actor.objects.create(name="Palin", age=27)
6028 Actor.objects.create(name="Kilbraken", age=50, title="Judge")
6029 response = self.client.get(reverse("admin:admin_views_sketch_add"))
6030 # Find the link
6031 m = re.search(
6032 br'<a href="([^"]*)"[^>]* id="lookup_id_defendant1"', response.content
6033 )
6034 self.assertTrue(m) # Got a match
6035 popup_url = m.groups()[0].decode().replace("&", "&")
6036
6037 # Handle relative links
6038 popup_url = urljoin(response.request["PATH_INFO"], popup_url)
6039 # Get the popup and verify the correct objects show up in the resulting
6040 # page. This step tests field__isnull=1 gets parsed correctly from the
6041 # lookup query string; in model we define defendant1 field to have a
6042 # limit_choices_to option that includes "actor__title__isnull=True".
6043 response2 = self.client.get(popup_url)
6044 self.assertNotContains(response2, "Kilbraken")
6045 self.assertContains(response2, "Palin")
6046
6047 def test_list_display_method_same_name_as_reverse_accessor(self):
6048 """
6049 Should be able to use a ModelAdmin method in list_display that has the
6050 same name as a reverse model field ("sketch" in this case).
6051 """
6052 actor = Actor.objects.create(name="Palin", age=27)
6053 Inquisition.objects.create(expected=True, leader=actor, country="England")
6054 response = self.client.get(reverse("admin:admin_views_inquisition_changelist"))
6055 self.assertContains(response, "list-display-sketch")
6056
6057
6058@override_settings(ROOT_URLCONF="admin_views.urls")
6059class UserAdminTest(TestCase):
6060 """
6061 Tests user CRUD functionality.
6062 """
6063
6064 @classmethod
6065 def setUpTestData(cls):
6066 cls.superuser = User.objects.create_superuser(
6067 username="super", password="secret", email="super@example.com"
6068 )
6069 cls.adduser = User.objects.create_user(
6070 username="adduser", password="secret", is_staff=True
6071 )
6072 cls.changeuser = User.objects.create_user(
6073 username="changeuser", password="secret", is_staff=True
6074 )
6075 cls.s1 = Section.objects.create(name="Test section")
6076 cls.a1 = Article.objects.create(
6077 content="<p>Middle content</p>",
6078 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
6079 section=cls.s1,
6080 )
6081 cls.a2 = Article.objects.create(
6082 content="<p>Oldest content</p>",
6083 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
6084 section=cls.s1,
6085 )
6086 cls.a3 = Article.objects.create(
6087 content="<p>Newest content</p>",
6088 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
6089 section=cls.s1,
6090 )
6091 cls.p1 = PrePopulatedPost.objects.create(
6092 title="A Long Title", published=True, slug="a-long-title"
6093 )
6094
6095 cls.per1 = Person.objects.create(name="John Mauchly", gender=1, alive=True)
6096 cls.per2 = Person.objects.create(name="Grace Hopper", gender=1, alive=False)
6097 cls.per3 = Person.objects.create(name="Guido van Rossum", gender=1, alive=True)
6098
6099 def setUp(self):
6100 self.client.force_login(self.superuser)
6101
6102 def test_save_button(self):
6103 user_count = User.objects.count()
6104 response = self.client.post(
6105 reverse("admin:auth_user_add"),
6106 {
6107 "username": "newuser",
6108 "password1": "newpassword",
6109 "password2": "newpassword",
6110 },
6111 )
6112 new_user = User.objects.get(username="newuser")
6113 self.assertRedirects(
6114 response, reverse("admin:auth_user_change", args=(new_user.pk,))
6115 )
6116 self.assertEqual(User.objects.count(), user_count + 1)
6117 self.assertTrue(new_user.has_usable_password())
6118
6119 def test_save_continue_editing_button(self):
6120 user_count = User.objects.count()
6121 response = self.client.post(
6122 reverse("admin:auth_user_add"),
6123 {
6124 "username": "newuser",
6125 "password1": "newpassword",
6126 "password2": "newpassword",
6127 "_continue": "1",
6128 },
6129 )
6130 new_user = User.objects.get(username="newuser")
6131 new_user_url = reverse("admin:auth_user_change", args=(new_user.pk,))
6132 self.assertRedirects(response, new_user_url, fetch_redirect_response=False)
6133 self.assertEqual(User.objects.count(), user_count + 1)
6134 self.assertTrue(new_user.has_usable_password())
6135 response = self.client.get(new_user_url)
6136 self.assertContains(
6137 response,
6138 '<li class="success">The user “<a href="%s">'
6139 "%s</a>” was added successfully. You may edit it again below.</li>"
6140 % (new_user_url, new_user),
6141 html=True,
6142 )
6143
6144 def test_password_mismatch(self):
6145 response = self.client.post(
6146 reverse("admin:auth_user_add"),
6147 {
6148 "username": "newuser",
6149 "password1": "newpassword",
6150 "password2": "mismatch",
6151 },
6152 )
6153 self.assertEqual(response.status_code, 200)
6154 self.assertFormError(response, "adminform", "password", [])
6155 self.assertFormError(
6156 response,
6157 "adminform",
6158 "password2",
6159 ["The two password fields didn’t match."],
6160 )
6161
6162 def test_user_fk_add_popup(self):
6163 """User addition through a FK popup should return the appropriate JavaScript response."""
6164 response = self.client.get(reverse("admin:admin_views_album_add"))
6165 self.assertContains(response, reverse("admin:auth_user_add"))
6166 self.assertContains(
6167 response,
6168 'class="related-widget-wrapper-link add-related" id="add_id_owner"',
6169 )
6170 response = self.client.get(reverse("admin:auth_user_add") + "?_popup=1")
6171 self.assertNotContains(response, 'name="_continue"')
6172 self.assertNotContains(response, 'name="_addanother"')
6173 data = {
6174 "username": "newuser",
6175 "password1": "newpassword",
6176 "password2": "newpassword",
6177 "_popup": "1",
6178 "_save": "1",
6179 }
6180 response = self.client.post(
6181 reverse("admin:auth_user_add") + "?_popup=1", data, follow=True
6182 )
6183 self.assertContains(response, ""obj": "newuser"")
6184
6185 def test_user_fk_change_popup(self):
6186 """User change through a FK popup should return the appropriate JavaScript response."""
6187 response = self.client.get(reverse("admin:admin_views_album_add"))
6188 self.assertContains(
6189 response, reverse("admin:auth_user_change", args=("__fk__",))
6190 )
6191 self.assertContains(
6192 response,
6193 'class="related-widget-wrapper-link change-related" id="change_id_owner"',
6194 )
6195 user = User.objects.get(username="changeuser")
6196 url = reverse("admin:auth_user_change", args=(user.pk,)) + "?_popup=1"
6197 response = self.client.get(url)
6198 self.assertNotContains(response, 'name="_continue"')
6199 self.assertNotContains(response, 'name="_addanother"')
6200 data = {
6201 "username": "newuser",
6202 "password1": "newpassword",
6203 "password2": "newpassword",
6204 "last_login_0": "2007-05-30",
6205 "last_login_1": "13:20:10",
6206 "date_joined_0": "2007-05-30",
6207 "date_joined_1": "13:20:10",
6208 "_popup": "1",
6209 "_save": "1",
6210 }
6211 response = self.client.post(url, data, follow=True)
6212 self.assertContains(response, ""obj": "newuser"")
6213 self.assertContains(response, ""action": "change"")
6214
6215 def test_user_fk_delete_popup(self):
6216 """User deletion through a FK popup should return the appropriate JavaScript response."""
6217 response = self.client.get(reverse("admin:admin_views_album_add"))
6218 self.assertContains(
6219 response, reverse("admin:auth_user_delete", args=("__fk__",))
6220 )
6221 self.assertContains(
6222 response,
6223 'class="related-widget-wrapper-link change-related" id="change_id_owner"',
6224 )
6225 user = User.objects.get(username="changeuser")
6226 url = reverse("admin:auth_user_delete", args=(user.pk,)) + "?_popup=1"
6227 response = self.client.get(url)
6228 self.assertEqual(response.status_code, 200)
6229 data = {
6230 "post": "yes",
6231 "_popup": "1",
6232 }
6233 response = self.client.post(url, data, follow=True)
6234 self.assertContains(response, ""action": "delete"")
6235
6236 def test_save_add_another_button(self):
6237 user_count = User.objects.count()
6238 response = self.client.post(
6239 reverse("admin:auth_user_add"),
6240 {
6241 "username": "newuser",
6242 "password1": "newpassword",
6243 "password2": "newpassword",
6244 "_addanother": "1",
6245 },
6246 )
6247 new_user = User.objects.order_by("-id")[0]
6248 self.assertRedirects(response, reverse("admin:auth_user_add"))
6249 self.assertEqual(User.objects.count(), user_count + 1)
6250 self.assertTrue(new_user.has_usable_password())
6251
6252 def test_user_permission_performance(self):
6253 u = User.objects.all()[0]
6254
6255 # Don't depend on a warm cache, see #17377.
6256 ContentType.objects.clear_cache()
6257
6258 with self.assertNumQueries(10):
6259 response = self.client.get(reverse("admin:auth_user_change", args=(u.pk,)))
6260 self.assertEqual(response.status_code, 200)
6261
6262 def test_form_url_present_in_context(self):
6263 u = User.objects.all()[0]
6264 response = self.client.get(
6265 reverse("admin3:auth_user_password_change", args=(u.pk,))
6266 )
6267 self.assertEqual(response.status_code, 200)
6268 self.assertEqual(response.context["form_url"], "pony")
6269
6270
6271@override_settings(ROOT_URLCONF="admin_views.urls")
6272class GroupAdminTest(TestCase):
6273 """
6274 Tests group CRUD functionality.
6275 """
6276
6277 @classmethod
6278 def setUpTestData(cls):
6279 cls.superuser = User.objects.create_superuser(
6280 username="super", password="secret", email="super@example.com"
6281 )
6282
6283 def setUp(self):
6284 self.client.force_login(self.superuser)
6285
6286 def test_save_button(self):
6287 group_count = Group.objects.count()
6288 response = self.client.post(
6289 reverse("admin:auth_group_add"), {"name": "newgroup",}
6290 )
6291
6292 Group.objects.order_by("-id")[0]
6293 self.assertRedirects(response, reverse("admin:auth_group_changelist"))
6294 self.assertEqual(Group.objects.count(), group_count + 1)
6295
6296 def test_group_permission_performance(self):
6297 g = Group.objects.create(name="test_group")
6298
6299 # Ensure no queries are skipped due to cached content type for Group.
6300 ContentType.objects.clear_cache()
6301
6302 with self.assertNumQueries(8):
6303 response = self.client.get(reverse("admin:auth_group_change", args=(g.pk,)))
6304 self.assertEqual(response.status_code, 200)
6305
6306
6307@override_settings(ROOT_URLCONF="admin_views.urls")
6308class CSSTest(TestCase):
6309 @classmethod
6310 def setUpTestData(cls):
6311 cls.superuser = User.objects.create_superuser(
6312 username="super", password="secret", email="super@example.com"
6313 )
6314 cls.s1 = Section.objects.create(name="Test section")
6315 cls.a1 = Article.objects.create(
6316 content="<p>Middle content</p>",
6317 date=datetime.datetime(2008, 3, 18, 11, 54, 58),
6318 section=cls.s1,
6319 )
6320 cls.a2 = Article.objects.create(
6321 content="<p>Oldest content</p>",
6322 date=datetime.datetime(2000, 3, 18, 11, 54, 58),
6323 section=cls.s1,
6324 )
6325 cls.a3 = Article.objects.create(
6326 content="<p>Newest content</p>",
6327 date=datetime.datetime(2009, 3, 18, 11, 54, 58),
6328 section=cls.s1,
6329 )
6330 cls.p1 = PrePopulatedPost.objects.create(
6331 title="A Long Title", published=True, slug="a-long-title"
6332 )
6333
6334 def setUp(self):
6335 self.client.force_login(self.superuser)
6336
6337 def test_field_prefix_css_classes(self):
6338 """
6339 Fields have a CSS class name with a 'field-' prefix.
6340 """
6341 response = self.client.get(reverse("admin:admin_views_post_add"))
6342
6343 # The main form
6344 self.assertContains(response, 'class="form-row field-title"')
6345 self.assertContains(response, 'class="form-row field-content"')
6346 self.assertContains(response, 'class="form-row field-public"')
6347 self.assertContains(response, 'class="form-row field-awesomeness_level"')
6348 self.assertContains(response, 'class="form-row field-coolness"')
6349 self.assertContains(response, 'class="form-row field-value"')
6350 self.assertContains(response, 'class="form-row"') # The lambda function
6351
6352 # The tabular inline
6353 self.assertContains(response, '<td class="field-url">')
6354 self.assertContains(response, '<td class="field-posted">')
6355
6356 def test_index_css_classes(self):
6357 """
6358 CSS class names are used for each app and model on the admin index
6359 pages (#17050).
6360 """
6361 # General index page
6362 response = self.client.get(reverse("admin:index"))
6363 self.assertContains(response, '<div class="app-admin_views module">')
6364 self.assertContains(response, '<tr class="model-actor">')
6365 self.assertContains(response, '<tr class="model-album">')
6366
6367 # App index page
6368 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6369 self.assertContains(response, '<div class="app-admin_views module">')
6370 self.assertContains(response, '<tr class="model-actor">')
6371 self.assertContains(response, '<tr class="model-album">')
6372
6373 def test_app_model_in_form_body_class(self):
6374 """
6375 Ensure app and model tag are correctly read by change_form template
6376 """
6377 response = self.client.get(reverse("admin:admin_views_section_add"))
6378 self.assertContains(response, '<body class=" app-admin_views model-section ')
6379
6380 def test_app_model_in_list_body_class(self):
6381 """
6382 Ensure app and model tag are correctly read by change_list template
6383 """
6384 response = self.client.get(reverse("admin:admin_views_section_changelist"))
6385 self.assertContains(response, '<body class=" app-admin_views model-section ')
6386
6387 def test_app_model_in_delete_confirmation_body_class(self):
6388 """
6389 Ensure app and model tag are correctly read by delete_confirmation
6390 template
6391 """
6392 response = self.client.get(
6393 reverse("admin:admin_views_section_delete", args=(self.s1.pk,))
6394 )
6395 self.assertContains(response, '<body class=" app-admin_views model-section ')
6396
6397 def test_app_model_in_app_index_body_class(self):
6398 """
6399 Ensure app and model tag are correctly read by app_index template
6400 """
6401 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6402 self.assertContains(response, '<body class=" dashboard app-admin_views')
6403
6404 def test_app_model_in_delete_selected_confirmation_body_class(self):
6405 """
6406 Ensure app and model tag are correctly read by
6407 delete_selected_confirmation template
6408 """
6409 action_data = {
6410 ACTION_CHECKBOX_NAME: [self.s1.pk],
6411 "action": "delete_selected",
6412 "index": 0,
6413 }
6414 response = self.client.post(
6415 reverse("admin:admin_views_section_changelist"), action_data
6416 )
6417 self.assertContains(response, '<body class=" app-admin_views model-section ')
6418
6419 def test_changelist_field_classes(self):
6420 """
6421 Cells of the change list table should contain the field name in their class attribute
6422 Refs #11195.
6423 """
6424 Podcast.objects.create(name="Django Dose", release_date=datetime.date.today())
6425 response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6426 self.assertContains(response, '<th class="field-name">')
6427 self.assertContains(response, '<td class="field-release_date nowrap">')
6428 self.assertContains(response, '<td class="action-checkbox">')
6429
6430
6431try:
6432 import docutils
6433except ImportError:
6434 docutils = None
6435
6436
6437@unittest.skipUnless(docutils, "no docutils installed.")
6438@override_settings(ROOT_URLCONF="admin_views.urls")
6439@modify_settings(
6440 INSTALLED_APPS={"append": ["django.contrib.admindocs", "django.contrib.flatpages"]}
6441)
6442class AdminDocsTest(TestCase):
6443 @classmethod
6444 def setUpTestData(cls):
6445 cls.superuser = User.objects.create_superuser(
6446 username="super", password="secret", email="super@example.com"
6447 )
6448
6449 def setUp(self):
6450 self.client.force_login(self.superuser)
6451
6452 def test_tags(self):
6453 response = self.client.get(reverse("django-admindocs-tags"))
6454
6455 # The builtin tag group exists
6456 self.assertContains(response, "<h2>Built-in tags</h2>", count=2, html=True)
6457
6458 # A builtin tag exists in both the index and detail
6459 self.assertContains(
6460 response, '<h3 id="built_in-autoescape">autoescape</h3>', html=True
6461 )
6462 self.assertContains(
6463 response,
6464 '<li><a href="#built_in-autoescape">autoescape</a></li>',
6465 html=True,
6466 )
6467
6468 # An app tag exists in both the index and detail
6469 self.assertContains(
6470 response, '<h3 id="flatpages-get_flatpages">get_flatpages</h3>', html=True
6471 )
6472 self.assertContains(
6473 response,
6474 '<li><a href="#flatpages-get_flatpages">get_flatpages</a></li>',
6475 html=True,
6476 )
6477
6478 # The admin list tag group exists
6479 self.assertContains(response, "<h2>admin_list</h2>", count=2, html=True)
6480
6481 # An admin list tag exists in both the index and detail
6482 self.assertContains(
6483 response, '<h3 id="admin_list-admin_actions">admin_actions</h3>', html=True
6484 )
6485 self.assertContains(
6486 response,
6487 '<li><a href="#admin_list-admin_actions">admin_actions</a></li>',
6488 html=True,
6489 )
6490
6491 def test_filters(self):
6492 response = self.client.get(reverse("django-admindocs-filters"))
6493
6494 # The builtin filter group exists
6495 self.assertContains(response, "<h2>Built-in filters</h2>", count=2, html=True)
6496
6497 # A builtin filter exists in both the index and detail
6498 self.assertContains(response, '<h3 id="built_in-add">add</h3>', html=True)
6499 self.assertContains(
6500 response, '<li><a href="#built_in-add">add</a></li>', html=True
6501 )
6502
6503
6504@override_settings(
6505 ROOT_URLCONF="admin_views.urls",
6506 TEMPLATES=[
6507 {
6508 "BACKEND": "django.template.backends.django.DjangoTemplates",
6509 "APP_DIRS": True,
6510 "OPTIONS": {
6511 "context_processors": [
6512 "django.template.context_processors.debug",
6513 "django.template.context_processors.request",
6514 "django.contrib.auth.context_processors.auth",
6515 "django.contrib.messages.context_processors.messages",
6516 ],
6517 },
6518 }
6519 ],
6520 USE_I18N=False,
6521)
6522class ValidXHTMLTests(TestCase):
6523 @classmethod
6524 def setUpTestData(cls):
6525 cls.superuser = User.objects.create_superuser(
6526 username="super", password="secret", email="super@example.com"
6527 )
6528
6529 def setUp(self):
6530 self.client.force_login(self.superuser)
6531
6532 def test_lang_name_present(self):
6533 response = self.client.get(reverse("admin:app_list", args=("admin_views",)))
6534 self.assertNotContains(response, ' lang=""')
6535 self.assertNotContains(response, ' xml:lang=""')
6536
6537
6538@override_settings(
6539 ROOT_URLCONF="admin_views.urls", USE_THOUSAND_SEPARATOR=True, USE_L10N=True
6540)
6541class DateHierarchyTests(TestCase):
6542 @classmethod
6543 def setUpTestData(cls):
6544 cls.superuser = User.objects.create_superuser(
6545 username="super", password="secret", email="super@example.com"
6546 )
6547
6548 def setUp(self):
6549 self.client.force_login(self.superuser)
6550
6551 def assert_non_localized_year(self, response, year):
6552 """
6553 The year is not localized with USE_THOUSAND_SEPARATOR (#15234).
6554 """
6555 self.assertNotContains(response, formats.number_format(year))
6556
6557 def assert_contains_year_link(self, response, date):
6558 self.assertContains(response, '?release_date__year=%d"' % (date.year,))
6559
6560 def assert_contains_month_link(self, response, date):
6561 self.assertContains(
6562 response,
6563 '?release_date__month=%d&release_date__year=%d"'
6564 % (date.month, date.year),
6565 )
6566
6567 def assert_contains_day_link(self, response, date):
6568 self.assertContains(
6569 response,
6570 "?release_date__day=%d&"
6571 'release_date__month=%d&release_date__year=%d"'
6572 % (date.day, date.month, date.year),
6573 )
6574
6575 def test_empty(self):
6576 """
6577 No date hierarchy links display with empty changelist.
6578 """
6579 response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6580 self.assertNotContains(response, "release_date__year=")
6581 self.assertNotContains(response, "release_date__month=")
6582 self.assertNotContains(response, "release_date__day=")
6583
6584 def test_single(self):
6585 """
6586 Single day-level date hierarchy appears for single object.
6587 """
6588 DATE = datetime.date(2000, 6, 30)
6589 Podcast.objects.create(release_date=DATE)
6590 url = reverse("admin:admin_views_podcast_changelist")
6591 response = self.client.get(url)
6592 self.assert_contains_day_link(response, DATE)
6593 self.assert_non_localized_year(response, 2000)
6594
6595 def test_within_month(self):
6596 """
6597 day-level links appear for changelist within single month.
6598 """
6599 DATES = (
6600 datetime.date(2000, 6, 30),
6601 datetime.date(2000, 6, 15),
6602 datetime.date(2000, 6, 3),
6603 )
6604 for date in DATES:
6605 Podcast.objects.create(release_date=date)
6606 url = reverse("admin:admin_views_podcast_changelist")
6607 response = self.client.get(url)
6608 for date in DATES:
6609 self.assert_contains_day_link(response, date)
6610 self.assert_non_localized_year(response, 2000)
6611
6612 def test_within_year(self):
6613 """
6614 month-level links appear for changelist within single year.
6615 """
6616 DATES = (
6617 datetime.date(2000, 1, 30),
6618 datetime.date(2000, 3, 15),
6619 datetime.date(2000, 5, 3),
6620 )
6621 for date in DATES:
6622 Podcast.objects.create(release_date=date)
6623 url = reverse("admin:admin_views_podcast_changelist")
6624 response = self.client.get(url)
6625 # no day-level links
6626 self.assertNotContains(response, "release_date__day=")
6627 for date in DATES:
6628 self.assert_contains_month_link(response, date)
6629 self.assert_non_localized_year(response, 2000)
6630
6631 def test_multiple_years(self):
6632 """
6633 year-level links appear for year-spanning changelist.
6634 """
6635 DATES = (
6636 datetime.date(2001, 1, 30),
6637 datetime.date(2003, 3, 15),
6638 datetime.date(2005, 5, 3),
6639 )
6640 for date in DATES:
6641 Podcast.objects.create(release_date=date)
6642 response = self.client.get(reverse("admin:admin_views_podcast_changelist"))
6643 # no day/month-level links
6644 self.assertNotContains(response, "release_date__day=")
6645 self.assertNotContains(response, "release_date__month=")
6646 for date in DATES:
6647 self.assert_contains_year_link(response, date)
6648
6649 # and make sure GET parameters still behave correctly
6650 for date in DATES:
6651 url = "%s?release_date__year=%d" % (
6652 reverse("admin:admin_views_podcast_changelist"),
6653 date.year,
6654 )
6655 response = self.client.get(url)
6656 self.assert_contains_month_link(response, date)
6657 self.assert_non_localized_year(response, 2000)
6658 self.assert_non_localized_year(response, 2003)
6659 self.assert_non_localized_year(response, 2005)
6660
6661 url = "%s?release_date__year=%d&release_date__month=%d" % (
6662 reverse("admin:admin_views_podcast_changelist"),
6663 date.year,
6664 date.month,
6665 )
6666 response = self.client.get(url)
6667 self.assert_contains_day_link(response, date)
6668 self.assert_non_localized_year(response, 2000)
6669 self.assert_non_localized_year(response, 2003)
6670 self.assert_non_localized_year(response, 2005)
6671
6672 def test_related_field(self):
6673 questions_data = (
6674 # (posted data, number of answers),
6675 (datetime.date(2001, 1, 30), 0),
6676 (datetime.date(2003, 3, 15), 1),
6677 (datetime.date(2005, 5, 3), 2),
6678 )
6679 for date, answer_count in questions_data:
6680 question = Question.objects.create(posted=date)
6681 for i in range(answer_count):
6682 question.answer_set.create()
6683
6684 response = self.client.get(reverse("admin:admin_views_answer_changelist"))
6685 for date, answer_count in questions_data:
6686 link = '?question__posted__year=%d"' % (date.year,)
6687 if answer_count > 0:
6688 self.assertContains(response, link)
6689 else:
6690 self.assertNotContains(response, link)
6691
6692
6693@override_settings(ROOT_URLCONF="admin_views.urls")
6694class AdminCustomSaveRelatedTests(TestCase):
6695 """
6696 One can easily customize the way related objects are saved.
6697 Refs #16115.
6698 """
6699
6700 @classmethod
6701 def setUpTestData(cls):
6702 cls.superuser = User.objects.create_superuser(
6703 username="super", password="secret", email="super@example.com"
6704 )
6705
6706 def setUp(self):
6707 self.client.force_login(self.superuser)
6708
6709 def test_should_be_able_to_edit_related_objects_on_add_view(self):
6710 post = {
6711 "child_set-TOTAL_FORMS": "3",
6712 "child_set-INITIAL_FORMS": "0",
6713 "name": "Josh Stone",
6714 "child_set-0-name": "Paul",
6715 "child_set-1-name": "Catherine",
6716 }
6717 self.client.post(reverse("admin:admin_views_parent_add"), post)
6718 self.assertEqual(1, Parent.objects.count())
6719 self.assertEqual(2, Child.objects.count())
6720
6721 children_names = list(
6722 Child.objects.order_by("name").values_list("name", flat=True)
6723 )
6724
6725 self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6726 self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6727
6728 def test_should_be_able_to_edit_related_objects_on_change_view(self):
6729 parent = Parent.objects.create(name="Josh Stone")
6730 paul = Child.objects.create(parent=parent, name="Paul")
6731 catherine = Child.objects.create(parent=parent, name="Catherine")
6732 post = {
6733 "child_set-TOTAL_FORMS": "5",
6734 "child_set-INITIAL_FORMS": "2",
6735 "name": "Josh Stone",
6736 "child_set-0-name": "Paul",
6737 "child_set-0-id": paul.id,
6738 "child_set-1-name": "Catherine",
6739 "child_set-1-id": catherine.id,
6740 }
6741 self.client.post(
6742 reverse("admin:admin_views_parent_change", args=(parent.id,)), post
6743 )
6744
6745 children_names = list(
6746 Child.objects.order_by("name").values_list("name", flat=True)
6747 )
6748
6749 self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6750 self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6751
6752 def test_should_be_able_to_edit_related_objects_on_changelist_view(self):
6753 parent = Parent.objects.create(name="Josh Rock")
6754 Child.objects.create(parent=parent, name="Paul")
6755 Child.objects.create(parent=parent, name="Catherine")
6756 post = {
6757 "form-TOTAL_FORMS": "1",
6758 "form-INITIAL_FORMS": "1",
6759 "form-MAX_NUM_FORMS": "0",
6760 "form-0-id": parent.id,
6761 "form-0-name": "Josh Stone",
6762 "_save": "Save",
6763 }
6764
6765 self.client.post(reverse("admin:admin_views_parent_changelist"), post)
6766 children_names = list(
6767 Child.objects.order_by("name").values_list("name", flat=True)
6768 )
6769
6770 self.assertEqual("Josh Stone", Parent.objects.latest("id").name)
6771 self.assertEqual(["Catherine Stone", "Paul Stone"], children_names)
6772
6773
6774@override_settings(ROOT_URLCONF="admin_views.urls")
6775class AdminViewLogoutTests(TestCase):
6776 @classmethod
6777 def setUpTestData(cls):
6778 cls.superuser = User.objects.create_superuser(
6779 username="super", password="secret", email="super@example.com"
6780 )
6781
6782 def test_logout(self):
6783 self.client.force_login(self.superuser)
6784 response = self.client.get(reverse("admin:logout"))
6785 self.assertEqual(response.status_code, 200)
6786 self.assertTemplateUsed(response, "registration/logged_out.html")
6787 self.assertEqual(response.request["PATH_INFO"], reverse("admin:logout"))
6788 self.assertFalse(response.context["has_permission"])
6789 self.assertNotContains(
6790 response, "user-tools"
6791 ) # user-tools div shouldn't visible.
6792
6793 def test_client_logout_url_can_be_used_to_login(self):
6794 response = self.client.get(reverse("admin:logout"))
6795 self.assertEqual(
6796 response.status_code, 302
6797 ) # we should be redirected to the login page.
6798
6799 # follow the redirect and test results.
6800 response = self.client.get(reverse("admin:logout"), follow=True)
6801 self.assertEqual(response.status_code, 200)
6802 self.assertTemplateUsed(response, "admin/login.html")
6803 self.assertEqual(response.request["PATH_INFO"], reverse("admin:login"))
6804 self.assertContains(
6805 response,
6806 '<input type="hidden" name="next" value="%s">' % reverse("admin:index"),
6807 )
6808
6809
6810@override_settings(ROOT_URLCONF="admin_views.urls")
6811class AdminUserMessageTest(TestCase):
6812 @classmethod
6813 def setUpTestData(cls):
6814 cls.superuser = User.objects.create_superuser(
6815 username="super", password="secret", email="super@example.com"
6816 )
6817
6818 def setUp(self):
6819 self.client.force_login(self.superuser)
6820
6821 def send_message(self, level):
6822 """
6823 Helper that sends a post to the dummy test methods and asserts that a
6824 message with the level has appeared in the response.
6825 """
6826 action_data = {
6827 ACTION_CHECKBOX_NAME: [1],
6828 "action": "message_%s" % level,
6829 "index": 0,
6830 }
6831
6832 response = self.client.post(
6833 reverse("admin:admin_views_usermessenger_changelist"),
6834 action_data,
6835 follow=True,
6836 )
6837 self.assertContains(
6838 response, '<li class="%s">Test %s</li>' % (level, level), html=True
6839 )
6840
6841 @override_settings(MESSAGE_LEVEL=10) # Set to DEBUG for this request
6842 def test_message_debug(self):
6843 self.send_message("debug")
6844
6845 def test_message_info(self):
6846 self.send_message("info")
6847
6848 def test_message_success(self):
6849 self.send_message("success")
6850
6851 def test_message_warning(self):
6852 self.send_message("warning")
6853
6854 def test_message_error(self):
6855 self.send_message("error")
6856
6857 def test_message_extra_tags(self):
6858 action_data = {
6859 ACTION_CHECKBOX_NAME: [1],
6860 "action": "message_extra_tags",
6861 "index": 0,
6862 }
6863
6864 response = self.client.post(
6865 reverse("admin:admin_views_usermessenger_changelist"),
6866 action_data,
6867 follow=True,
6868 )
6869 self.assertContains(
6870 response, '<li class="extra_tag info">Test tags</li>', html=True
6871 )
6872
6873
6874@override_settings(ROOT_URLCONF="admin_views.urls")
6875class AdminKeepChangeListFiltersTests(TestCase):
6876 admin_site = site
6877
6878 @classmethod
6879 def setUpTestData(cls):
6880 cls.superuser = User.objects.create_superuser(
6881 username="super", password="secret", email="super@example.com"
6882 )
6883 cls.joepublicuser = User.objects.create_user(
6884 username="joepublic", password="secret"
6885 )
6886
6887 def setUp(self):
6888 self.client.force_login(self.superuser)
6889
6890 def assertURLEqual(self, url1, url2, msg_prefix=""):
6891 """
6892 Assert that two URLs are equal despite the ordering
6893 of their querystring. Refs #22360.
6894 """
6895 parsed_url1 = urlparse(url1)
6896 path1 = parsed_url1.path
6897 parsed_qs1 = dict(parse_qsl(parsed_url1.query))
6898
6899 parsed_url2 = urlparse(url2)
6900 path2 = parsed_url2.path
6901 parsed_qs2 = dict(parse_qsl(parsed_url2.query))
6902
6903 for parsed_qs in [parsed_qs1, parsed_qs2]:
6904 if "_changelist_filters" in parsed_qs:
6905 changelist_filters = parsed_qs["_changelist_filters"]
6906 parsed_filters = dict(parse_qsl(changelist_filters))
6907 parsed_qs["_changelist_filters"] = parsed_filters
6908
6909 self.assertEqual(path1, path2)
6910 self.assertEqual(parsed_qs1, parsed_qs2)
6911
6912 def test_assert_url_equal(self):
6913 # Test equality.
6914 change_user_url = reverse(
6915 "admin:auth_user_change", args=(self.joepublicuser.pk,)
6916 )
6917 self.assertURLEqual(
6918 "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6919 change_user_url
6920 ),
6921 "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6922 change_user_url
6923 ),
6924 )
6925
6926 # Test inequality.
6927 with self.assertRaises(AssertionError):
6928 self.assertURLEqual(
6929 "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6930 change_user_url
6931 ),
6932 "http://testserver{}?_changelist_filters=is_staff__exact%3D1%26is_superuser__exact%3D1".format(
6933 change_user_url
6934 ),
6935 )
6936
6937 # Ignore scheme and host.
6938 self.assertURLEqual(
6939 "http://testserver{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6940 change_user_url
6941 ),
6942 "{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6943 change_user_url
6944 ),
6945 )
6946
6947 # Ignore ordering of querystring.
6948 self.assertURLEqual(
6949 "{}?is_staff__exact=0&is_superuser__exact=0".format(
6950 reverse("admin:auth_user_changelist")
6951 ),
6952 "{}?is_superuser__exact=0&is_staff__exact=0".format(
6953 reverse("admin:auth_user_changelist")
6954 ),
6955 )
6956
6957 # Ignore ordering of _changelist_filters.
6958 self.assertURLEqual(
6959 "{}?_changelist_filters=is_staff__exact%3D0%26is_superuser__exact%3D0".format(
6960 change_user_url
6961 ),
6962 "{}?_changelist_filters=is_superuser__exact%3D0%26is_staff__exact%3D0".format(
6963 change_user_url
6964 ),
6965 )
6966
6967 def get_changelist_filters(self):
6968 return {
6969 "is_superuser__exact": 0,
6970 "is_staff__exact": 0,
6971 }
6972
6973 def get_changelist_filters_querystring(self):
6974 return urlencode(self.get_changelist_filters())
6975
6976 def get_preserved_filters_querystring(self):
6977 return urlencode(
6978 {"_changelist_filters": self.get_changelist_filters_querystring()}
6979 )
6980
6981 def get_sample_user_id(self):
6982 return self.joepublicuser.pk
6983
6984 def get_changelist_url(self):
6985 return "%s?%s" % (
6986 reverse("admin:auth_user_changelist", current_app=self.admin_site.name),
6987 self.get_changelist_filters_querystring(),
6988 )
6989
6990 def get_add_url(self):
6991 return "%s?%s" % (
6992 reverse("admin:auth_user_add", current_app=self.admin_site.name),
6993 self.get_preserved_filters_querystring(),
6994 )
6995
6996 def get_change_url(self, user_id=None):
6997 if user_id is None:
6998 user_id = self.get_sample_user_id()
6999 return "%s?%s" % (
7000 reverse(
7001 "admin:auth_user_change",
7002 args=(user_id,),
7003 current_app=self.admin_site.name,
7004 ),
7005 self.get_preserved_filters_querystring(),
7006 )
7007
7008 def get_history_url(self, user_id=None):
7009 if user_id is None:
7010 user_id = self.get_sample_user_id()
7011 return "%s?%s" % (
7012 reverse(
7013 "admin:auth_user_history",
7014 args=(user_id,),
7015 current_app=self.admin_site.name,
7016 ),
7017 self.get_preserved_filters_querystring(),
7018 )
7019
7020 def get_delete_url(self, user_id=None):
7021 if user_id is None:
7022 user_id = self.get_sample_user_id()
7023 return "%s?%s" % (
7024 reverse(
7025 "admin:auth_user_delete",
7026 args=(user_id,),
7027 current_app=self.admin_site.name,
7028 ),
7029 self.get_preserved_filters_querystring(),
7030 )
7031
7032 def test_changelist_view(self):
7033 response = self.client.get(self.get_changelist_url())
7034 self.assertEqual(response.status_code, 200)
7035
7036 # Check the `change_view` link has the correct querystring.
7037 detail_link = re.search(
7038 '<a href="(.*?)">{}</a>'.format(self.joepublicuser.username),
7039 response.content.decode(),
7040 )
7041 self.assertURLEqual(detail_link.group(1), self.get_change_url())
7042
7043 def test_change_view(self):
7044 # Get the `change_view`.
7045 response = self.client.get(self.get_change_url())
7046 self.assertEqual(response.status_code, 200)
7047
7048 # Check the form action.
7049 form_action = re.search(
7050 '<form action="(.*?)" method="post" id="user_form".*?>',
7051 response.content.decode(),
7052 )
7053 self.assertURLEqual(
7054 form_action.group(1), "?%s" % self.get_preserved_filters_querystring()
7055 )
7056
7057 # Check the history link.
7058 history_link = re.search(
7059 '<a href="(.*?)" class="historylink">History</a>', response.content.decode()
7060 )
7061 self.assertURLEqual(history_link.group(1), self.get_history_url())
7062
7063 # Check the delete link.
7064 delete_link = re.search(
7065 '<a href="(.*?)" class="deletelink">Delete</a>', response.content.decode()
7066 )
7067 self.assertURLEqual(delete_link.group(1), self.get_delete_url())
7068
7069 # Test redirect on "Save".
7070 post_data = {
7071 "username": "joepublic",
7072 "last_login_0": "2007-05-30",
7073 "last_login_1": "13:20:10",
7074 "date_joined_0": "2007-05-30",
7075 "date_joined_1": "13:20:10",
7076 }
7077
7078 post_data["_save"] = 1
7079 response = self.client.post(self.get_change_url(), data=post_data)
7080 self.assertRedirects(response, self.get_changelist_url())
7081 post_data.pop("_save")
7082
7083 # Test redirect on "Save and continue".
7084 post_data["_continue"] = 1
7085 response = self.client.post(self.get_change_url(), data=post_data)
7086 self.assertRedirects(response, self.get_change_url())
7087 post_data.pop("_continue")
7088
7089 # Test redirect on "Save and add new".
7090 post_data["_addanother"] = 1
7091 response = self.client.post(self.get_change_url(), data=post_data)
7092 self.assertRedirects(response, self.get_add_url())
7093 post_data.pop("_addanother")
7094
7095 def test_add_view(self):
7096 # Get the `add_view`.
7097 response = self.client.get(self.get_add_url())
7098 self.assertEqual(response.status_code, 200)
7099
7100 # Check the form action.
7101 form_action = re.search(
7102 '<form action="(.*?)" method="post" id="user_form".*?>',
7103 response.content.decode(),
7104 )
7105 self.assertURLEqual(
7106 form_action.group(1), "?%s" % self.get_preserved_filters_querystring()
7107 )
7108
7109 post_data = {
7110 "username": "dummy",
7111 "password1": "test",
7112 "password2": "test",
7113 }
7114
7115 # Test redirect on "Save".
7116 post_data["_save"] = 1
7117 response = self.client.post(self.get_add_url(), data=post_data)
7118 self.assertRedirects(
7119 response, self.get_change_url(User.objects.get(username="dummy").pk)
7120 )
7121 post_data.pop("_save")
7122
7123 # Test redirect on "Save and continue".
7124 post_data["username"] = "dummy2"
7125 post_data["_continue"] = 1
7126 response = self.client.post(self.get_add_url(), data=post_data)
7127 self.assertRedirects(
7128 response, self.get_change_url(User.objects.get(username="dummy2").pk)
7129 )
7130 post_data.pop("_continue")
7131
7132 # Test redirect on "Save and add new".
7133 post_data["username"] = "dummy3"
7134 post_data["_addanother"] = 1
7135 response = self.client.post(self.get_add_url(), data=post_data)
7136 self.assertRedirects(response, self.get_add_url())
7137 post_data.pop("_addanother")
7138
7139 def test_delete_view(self):
7140 # Test redirect on "Delete".
7141 response = self.client.post(self.get_delete_url(), {"post": "yes"})
7142 self.assertRedirects(response, self.get_changelist_url())
7143
7144 def test_url_prefix(self):
7145 context = {
7146 "preserved_filters": self.get_preserved_filters_querystring(),
7147 "opts": User._meta,
7148 }
7149 prefixes = ("", "/prefix/", "/後台/")
7150 for prefix in prefixes:
7151 with self.subTest(prefix=prefix), override_script_prefix(prefix):
7152 url = reverse(
7153 "admin:auth_user_changelist", current_app=self.admin_site.name
7154 )
7155 self.assertURLEqual(
7156 self.get_changelist_url(), add_preserved_filters(context, url),
7157 )
7158
7159
7160class NamespacedAdminKeepChangeListFiltersTests(AdminKeepChangeListFiltersTests):
7161 admin_site = site2
7162
7163
7164@override_settings(ROOT_URLCONF="admin_views.urls")
7165class TestLabelVisibility(TestCase):
7166 """ #11277 -Labels of hidden fields in admin were not hidden. """
7167
7168 @classmethod
7169 def setUpTestData(cls):
7170 cls.superuser = User.objects.create_superuser(
7171 username="super", password="secret", email="super@example.com"
7172 )
7173
7174 def setUp(self):
7175 self.client.force_login(self.superuser)
7176
7177 def test_all_fields_visible(self):
7178 response = self.client.get(reverse("admin:admin_views_emptymodelvisible_add"))
7179 self.assert_fieldline_visible(response)
7180 self.assert_field_visible(response, "first")
7181 self.assert_field_visible(response, "second")
7182
7183 def test_all_fields_hidden(self):
7184 response = self.client.get(reverse("admin:admin_views_emptymodelhidden_add"))
7185 self.assert_fieldline_hidden(response)
7186 self.assert_field_hidden(response, "first")
7187 self.assert_field_hidden(response, "second")
7188
7189 def test_mixin(self):
7190 response = self.client.get(reverse("admin:admin_views_emptymodelmixin_add"))
7191 self.assert_fieldline_visible(response)
7192 self.assert_field_hidden(response, "first")
7193 self.assert_field_visible(response, "second")
7194
7195 def assert_field_visible(self, response, field_name):
7196 self.assertContains(response, '<div class="fieldBox field-%s">' % field_name)
7197
7198 def assert_field_hidden(self, response, field_name):
7199 self.assertContains(
7200 response, '<div class="fieldBox field-%s hidden">' % field_name
7201 )
7202
7203 def assert_fieldline_visible(self, response):
7204 self.assertContains(response, '<div class="form-row field-first field-second">')
7205
7206 def assert_fieldline_hidden(self, response):
7207 self.assertContains(response, '<div class="form-row hidden')
7208
7209
7210@override_settings(ROOT_URLCONF="admin_views.urls")
7211class AdminViewOnSiteTests(TestCase):
7212 @classmethod
7213 def setUpTestData(cls):
7214 cls.superuser = User.objects.create_superuser(
7215 username="super", password="secret", email="super@example.com"
7216 )
7217
7218 cls.s1 = State.objects.create(name="New York")
7219 cls.s2 = State.objects.create(name="Illinois")
7220 cls.s3 = State.objects.create(name="California")
7221 cls.c1 = City.objects.create(state=cls.s1, name="New York")
7222 cls.c2 = City.objects.create(state=cls.s2, name="Chicago")
7223 cls.c3 = City.objects.create(state=cls.s3, name="San Francisco")
7224 cls.r1 = Restaurant.objects.create(city=cls.c1, name="Italian Pizza")
7225 cls.r2 = Restaurant.objects.create(city=cls.c1, name="Boulevard")
7226 cls.r3 = Restaurant.objects.create(city=cls.c2, name="Chinese Dinner")
7227 cls.r4 = Restaurant.objects.create(city=cls.c2, name="Angels")
7228 cls.r5 = Restaurant.objects.create(city=cls.c2, name="Take Away")
7229 cls.r6 = Restaurant.objects.create(city=cls.c3, name="The Unknown Restaurant")
7230 cls.w1 = Worker.objects.create(work_at=cls.r1, name="Mario", surname="Rossi")
7231 cls.w2 = Worker.objects.create(
7232 work_at=cls.r1, name="Antonio", surname="Bianchi"
7233 )
7234 cls.w3 = Worker.objects.create(work_at=cls.r1, name="John", surname="Doe")
7235
7236 def setUp(self):
7237 self.client.force_login(self.superuser)
7238
7239 def test_add_view_form_and_formsets_run_validation(self):
7240 """
7241 Issue #20522
7242 Verifying that if the parent form fails validation, the inlines also
7243 run validation even if validation is contingent on parent form data.
7244 Also, assertFormError() and assertFormsetError() is usable for admin
7245 forms and formsets.
7246 """
7247 # The form validation should fail because 'some_required_info' is
7248 # not included on the parent form, and the family_name of the parent
7249 # does not match that of the child
7250 post_data = {
7251 "family_name": "Test1",
7252 "dependentchild_set-TOTAL_FORMS": "1",
7253 "dependentchild_set-INITIAL_FORMS": "0",
7254 "dependentchild_set-MAX_NUM_FORMS": "1",
7255 "dependentchild_set-0-id": "",
7256 "dependentchild_set-0-parent": "",
7257 "dependentchild_set-0-family_name": "Test2",
7258 }
7259 response = self.client.post(
7260 reverse("admin:admin_views_parentwithdependentchildren_add"), post_data
7261 )
7262 self.assertFormError(
7263 response, "adminform", "some_required_info", ["This field is required."]
7264 )
7265 msg = "The form 'adminform' in context 0 does not contain the non-field error 'Error'"
7266 with self.assertRaisesMessage(AssertionError, msg):
7267 self.assertFormError(response, "adminform", None, ["Error"])
7268 self.assertFormsetError(
7269 response,
7270 "inline_admin_formset",
7271 0,
7272 None,
7273 [
7274 "Children must share a family name with their parents in this contrived test case"
7275 ],
7276 )
7277 msg = "The formset 'inline_admin_formset' in context 10 does not contain any non-form errors."
7278 with self.assertRaisesMessage(AssertionError, msg):
7279 self.assertFormsetError(
7280 response, "inline_admin_formset", None, None, ["Error"]
7281 )
7282
7283 def test_change_view_form_and_formsets_run_validation(self):
7284 """
7285 Issue #20522
7286 Verifying that if the parent form fails validation, the inlines also
7287 run validation even if validation is contingent on parent form data
7288 """
7289 pwdc = ParentWithDependentChildren.objects.create(
7290 some_required_info=6, family_name="Test1"
7291 )
7292 # The form validation should fail because 'some_required_info' is
7293 # not included on the parent form, and the family_name of the parent
7294 # does not match that of the child
7295 post_data = {
7296 "family_name": "Test2",
7297 "dependentchild_set-TOTAL_FORMS": "1",
7298 "dependentchild_set-INITIAL_FORMS": "0",
7299 "dependentchild_set-MAX_NUM_FORMS": "1",
7300 "dependentchild_set-0-id": "",
7301 "dependentchild_set-0-parent": str(pwdc.id),
7302 "dependentchild_set-0-family_name": "Test1",
7303 }
7304 response = self.client.post(
7305 reverse(
7306 "admin:admin_views_parentwithdependentchildren_change", args=(pwdc.id,)
7307 ),
7308 post_data,
7309 )
7310 self.assertFormError(
7311 response, "adminform", "some_required_info", ["This field is required."]
7312 )
7313 self.assertFormsetError(
7314 response,
7315 "inline_admin_formset",
7316 0,
7317 None,
7318 [
7319 "Children must share a family name with their parents in this contrived test case"
7320 ],
7321 )
7322
7323 def test_check(self):
7324 "The view_on_site value is either a boolean or a callable"
7325 try:
7326 admin = CityAdmin(City, AdminSite())
7327 CityAdmin.view_on_site = True
7328 self.assertEqual(admin.check(), [])
7329 CityAdmin.view_on_site = False
7330 self.assertEqual(admin.check(), [])
7331 CityAdmin.view_on_site = lambda obj: obj.get_absolute_url()
7332 self.assertEqual(admin.check(), [])
7333 CityAdmin.view_on_site = []
7334 self.assertEqual(
7335 admin.check(),
7336 [
7337 Error(
7338 "The value of 'view_on_site' must be a callable or a boolean value.",
7339 obj=CityAdmin,
7340 id="admin.E025",
7341 ),
7342 ],
7343 )
7344 finally:
7345 # Restore the original values for the benefit of other tests.
7346 CityAdmin.view_on_site = True
7347
7348 def test_false(self):
7349 "The 'View on site' button is not displayed if view_on_site is False"
7350 response = self.client.get(
7351 reverse("admin:admin_views_restaurant_change", args=(self.r1.pk,))
7352 )
7353 content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
7354 self.assertNotContains(
7355 response, reverse("admin:view_on_site", args=(content_type_pk, 1))
7356 )
7357
7358 def test_true(self):
7359 "The default behavior is followed if view_on_site is True"
7360 response = self.client.get(
7361 reverse("admin:admin_views_city_change", args=(self.c1.pk,))
7362 )
7363 content_type_pk = ContentType.objects.get_for_model(City).pk
7364 self.assertContains(
7365 response, reverse("admin:view_on_site", args=(content_type_pk, self.c1.pk))
7366 )
7367
7368 def test_callable(self):
7369 "The right link is displayed if view_on_site is a callable"
7370 response = self.client.get(
7371 reverse("admin:admin_views_worker_change", args=(self.w1.pk,))
7372 )
7373 self.assertContains(
7374 response, '"/worker/%s/%s/"' % (self.w1.surname, self.w1.name)
7375 )
7376
7377 def test_missing_get_absolute_url(self):
7378 "None is returned if model doesn't have get_absolute_url"
7379 model_admin = ModelAdmin(Worker, None)
7380 self.assertIsNone(model_admin.get_view_on_site_url(Worker()))
7381
7382
7383@override_settings(ROOT_URLCONF="admin_views.urls")
7384class InlineAdminViewOnSiteTest(TestCase):
7385 @classmethod
7386 def setUpTestData(cls):
7387 cls.superuser = User.objects.create_superuser(
7388 username="super", password="secret", email="super@example.com"
7389 )
7390
7391 cls.s1 = State.objects.create(name="New York")
7392 cls.s2 = State.objects.create(name="Illinois")
7393 cls.s3 = State.objects.create(name="California")
7394 cls.c1 = City.objects.create(state=cls.s1, name="New York")
7395 cls.c2 = City.objects.create(state=cls.s2, name="Chicago")
7396 cls.c3 = City.objects.create(state=cls.s3, name="San Francisco")
7397 cls.r1 = Restaurant.objects.create(city=cls.c1, name="Italian Pizza")
7398 cls.r2 = Restaurant.objects.create(city=cls.c1, name="Boulevard")
7399 cls.r3 = Restaurant.objects.create(city=cls.c2, name="Chinese Dinner")
7400 cls.r4 = Restaurant.objects.create(city=cls.c2, name="Angels")
7401 cls.r5 = Restaurant.objects.create(city=cls.c2, name="Take Away")
7402 cls.r6 = Restaurant.objects.create(city=cls.c3, name="The Unknown Restaurant")
7403 cls.w1 = Worker.objects.create(work_at=cls.r1, name="Mario", surname="Rossi")
7404 cls.w2 = Worker.objects.create(
7405 work_at=cls.r1, name="Antonio", surname="Bianchi"
7406 )
7407 cls.w3 = Worker.objects.create(work_at=cls.r1, name="John", surname="Doe")
7408
7409 def setUp(self):
7410 self.client.force_login(self.superuser)
7411
7412 def test_false(self):
7413 "The 'View on site' button is not displayed if view_on_site is False"
7414 response = self.client.get(
7415 reverse("admin:admin_views_state_change", args=(self.s1.pk,))
7416 )
7417 content_type_pk = ContentType.objects.get_for_model(City).pk
7418 self.assertNotContains(
7419 response, reverse("admin:view_on_site", args=(content_type_pk, self.c1.pk))
7420 )
7421
7422 def test_true(self):
7423 "The 'View on site' button is displayed if view_on_site is True"
7424 response = self.client.get(
7425 reverse("admin:admin_views_city_change", args=(self.c1.pk,))
7426 )
7427 content_type_pk = ContentType.objects.get_for_model(Restaurant).pk
7428 self.assertContains(
7429 response, reverse("admin:view_on_site", args=(content_type_pk, self.r1.pk))
7430 )
7431
7432 def test_callable(self):
7433 "The right link is displayed if view_on_site is a callable"
7434 response = self.client.get(
7435 reverse("admin:admin_views_restaurant_change", args=(self.r1.pk,))
7436 )
7437 self.assertContains(
7438 response, '"/worker_inline/%s/%s/"' % (self.w1.surname, self.w1.name)
7439 )
7440
7441
7442@override_settings(ROOT_URLCONF="admin_views.urls")
7443class GetFormsetsWithInlinesArgumentTest(TestCase):
7444 """
7445 #23934 - When adding a new model instance in the admin, the 'obj' argument
7446 of get_formsets_with_inlines() should be None. When changing, it should be
7447 equal to the existing model instance.
7448 The GetFormsetsArgumentCheckingAdmin ModelAdmin throws an exception
7449 if obj is not None during add_view or obj is None during change_view.
7450 """
7451
7452 @classmethod
7453 def setUpTestData(cls):
7454 cls.superuser = User.objects.create_superuser(
7455 username="super", password="secret", email="super@example.com"
7456 )
7457
7458 def setUp(self):
7459 self.client.force_login(self.superuser)
7460
7461 def test_explicitly_provided_pk(self):
7462 post_data = {"name": "1"}
7463 response = self.client.post(
7464 reverse("admin:admin_views_explicitlyprovidedpk_add"), post_data
7465 )
7466 self.assertEqual(response.status_code, 302)
7467
7468 post_data = {"name": "2"}
7469 response = self.client.post(
7470 reverse("admin:admin_views_explicitlyprovidedpk_change", args=(1,)),
7471 post_data,
7472 )
7473 self.assertEqual(response.status_code, 302)
7474
7475 def test_implicitly_generated_pk(self):
7476 post_data = {"name": "1"}
7477 response = self.client.post(
7478 reverse("admin:admin_views_implicitlygeneratedpk_add"), post_data
7479 )
7480 self.assertEqual(response.status_code, 302)
7481
7482 post_data = {"name": "2"}
7483 response = self.client.post(
7484 reverse("admin:admin_views_implicitlygeneratedpk_change", args=(1,)),
7485 post_data,
7486 )
7487 self.assertEqual(response.status_code, 302)