django.contrib.auth (django/contrib/auth/backends.py)

Description

  • The new reset_url_token attribute in PasswordResetConfirmView allows specifying a token parameter displayed as a component of password reset URLs.

  • Added BaseBackend class to ease customization of authentication backends.

  • Added get_user_permissions() method to mirror the existing get_group_permissions() method.

  • Added HTML autocomplete attribute to widgets of username, email, and password fields in django.contrib.auth.forms for better interaction with browser password managers.

  • createsuperuser now falls back to environment variables for password and required fields, when a corresponding command line argument isn’t provided in non-interactive mode.

  • REQUIRED_FIELDS now supports ManyToManyFields.

  • The new UserManager.with_perm() method returns users that have the specified permission.

  • The default iteration count for the PBKDF2 password hasher is increased from 150,000 to 180,000 .

django/conf/global_settings.py

  1"""
  2Default Django settings. Override these with settings in the module pointed to
  3by the DJANGO_SETTINGS_MODULE environment variable.
  4"""
  5
  6
  7# This is defined here as a do-nothing function because we can't import
  8# django.utils.translation -- that module depends on the settings.
  9def gettext_noop(s):
 10    return s
 11
 12
 13####################
 14# CORE             #
 15####################
 16
 17DEBUG = False
 18
 19# Whether the framework should propagate raw exceptions rather than catching
 20# them. This is useful under some testing situations and should never be used
 21# on a live site.
 22DEBUG_PROPAGATE_EXCEPTIONS = False
 23
 24# People who get code error notifications.
 25# In the format [('Full Name', 'email@example.com'), ('Full Name', 'anotheremail@example.com')]
 26ADMINS = []
 27
 28# List of IP addresses, as strings, that:
 29#   * See debug comments, when DEBUG is true
 30#   * Receive x-headers
 31INTERNAL_IPS = []
 32
 33# Hosts/domain names that are valid for this site.
 34# "*" matches anything, ".example.com" matches example.com and all subdomains
 35ALLOWED_HOSTS = []
 36
 37# Local time zone for this installation. All choices can be found here:
 38# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
 39# systems may support all possibilities). When USE_TZ is True, this is
 40# interpreted as the default user time zone.
 41TIME_ZONE = "America/Chicago"
 42
 43# If you set this to True, Django will use timezone-aware datetimes.
 44USE_TZ = False
 45
 46# Language code for this installation. All choices can be found here:
 47# http://www.i18nguy.com/unicode/language-identifiers.html
 48LANGUAGE_CODE = "en-us"
 49
 50# Languages we provide translations for, out of the box.
 51LANGUAGES = [
 52    ("af", gettext_noop("Afrikaans")),
 53    ("ar", gettext_noop("Arabic")),
 54    ("ast", gettext_noop("Asturian")),
 55    ("az", gettext_noop("Azerbaijani")),
 56    ("bg", gettext_noop("Bulgarian")),
 57    ("be", gettext_noop("Belarusian")),
 58    ("bn", gettext_noop("Bengali")),
 59    ("br", gettext_noop("Breton")),
 60    ("bs", gettext_noop("Bosnian")),
 61    ("ca", gettext_noop("Catalan")),
 62    ("cs", gettext_noop("Czech")),
 63    ("cy", gettext_noop("Welsh")),
 64    ("da", gettext_noop("Danish")),
 65    ("de", gettext_noop("German")),
 66    ("dsb", gettext_noop("Lower Sorbian")),
 67    ("el", gettext_noop("Greek")),
 68    ("en", gettext_noop("English")),
 69    ("en-au", gettext_noop("Australian English")),
 70    ("en-gb", gettext_noop("British English")),
 71    ("eo", gettext_noop("Esperanto")),
 72    ("es", gettext_noop("Spanish")),
 73    ("es-ar", gettext_noop("Argentinian Spanish")),
 74    ("es-co", gettext_noop("Colombian Spanish")),
 75    ("es-mx", gettext_noop("Mexican Spanish")),
 76    ("es-ni", gettext_noop("Nicaraguan Spanish")),
 77    ("es-ve", gettext_noop("Venezuelan Spanish")),
 78    ("et", gettext_noop("Estonian")),
 79    ("eu", gettext_noop("Basque")),
 80    ("fa", gettext_noop("Persian")),
 81    ("fi", gettext_noop("Finnish")),
 82    ("fr", gettext_noop("French")),
 83    ("fy", gettext_noop("Frisian")),
 84    ("ga", gettext_noop("Irish")),
 85    ("gd", gettext_noop("Scottish Gaelic")),
 86    ("gl", gettext_noop("Galician")),
 87    ("he", gettext_noop("Hebrew")),
 88    ("hi", gettext_noop("Hindi")),
 89    ("hr", gettext_noop("Croatian")),
 90    ("hsb", gettext_noop("Upper Sorbian")),
 91    ("hu", gettext_noop("Hungarian")),
 92    ("hy", gettext_noop("Armenian")),
 93    ("ia", gettext_noop("Interlingua")),
 94    ("id", gettext_noop("Indonesian")),
 95    ("io", gettext_noop("Ido")),
 96    ("is", gettext_noop("Icelandic")),
 97    ("it", gettext_noop("Italian")),
 98    ("ja", gettext_noop("Japanese")),
 99    ("ka", gettext_noop("Georgian")),
100    ("kab", gettext_noop("Kabyle")),
101    ("kk", gettext_noop("Kazakh")),
102    ("km", gettext_noop("Khmer")),
103    ("kn", gettext_noop("Kannada")),
104    ("ko", gettext_noop("Korean")),
105    ("lb", gettext_noop("Luxembourgish")),
106    ("lt", gettext_noop("Lithuanian")),
107    ("lv", gettext_noop("Latvian")),
108    ("mk", gettext_noop("Macedonian")),
109    ("ml", gettext_noop("Malayalam")),
110    ("mn", gettext_noop("Mongolian")),
111    ("mr", gettext_noop("Marathi")),
112    ("my", gettext_noop("Burmese")),
113    ("nb", gettext_noop("Norwegian Bokmål")),
114    ("ne", gettext_noop("Nepali")),
115    ("nl", gettext_noop("Dutch")),
116    ("nn", gettext_noop("Norwegian Nynorsk")),
117    ("os", gettext_noop("Ossetic")),
118    ("pa", gettext_noop("Punjabi")),
119    ("pl", gettext_noop("Polish")),
120    ("pt", gettext_noop("Portuguese")),
121    ("pt-br", gettext_noop("Brazilian Portuguese")),
122    ("ro", gettext_noop("Romanian")),
123    ("ru", gettext_noop("Russian")),
124    ("sk", gettext_noop("Slovak")),
125    ("sl", gettext_noop("Slovenian")),
126    ("sq", gettext_noop("Albanian")),
127    ("sr", gettext_noop("Serbian")),
128    ("sr-latn", gettext_noop("Serbian Latin")),
129    ("sv", gettext_noop("Swedish")),
130    ("sw", gettext_noop("Swahili")),
131    ("ta", gettext_noop("Tamil")),
132    ("te", gettext_noop("Telugu")),
133    ("th", gettext_noop("Thai")),
134    ("tr", gettext_noop("Turkish")),
135    ("tt", gettext_noop("Tatar")),
136    ("udm", gettext_noop("Udmurt")),
137    ("uk", gettext_noop("Ukrainian")),
138    ("ur", gettext_noop("Urdu")),
139    ("uz", gettext_noop("Uzbek")),
140    ("vi", gettext_noop("Vietnamese")),
141    ("zh-hans", gettext_noop("Simplified Chinese")),
142    ("zh-hant", gettext_noop("Traditional Chinese")),
143]
144
145# Languages using BiDi (right-to-left) layout
146LANGUAGES_BIDI = ["he", "ar", "fa", "ur"]
147
148# If you set this to False, Django will make some optimizations so as not
149# to load the internationalization machinery.
150USE_I18N = True
151LOCALE_PATHS = []
152
153# Settings for language cookie
154LANGUAGE_COOKIE_NAME = "django_language"
155LANGUAGE_COOKIE_AGE = None
156LANGUAGE_COOKIE_DOMAIN = None
157LANGUAGE_COOKIE_PATH = "/"
158LANGUAGE_COOKIE_SECURE = False
159LANGUAGE_COOKIE_HTTPONLY = False
160LANGUAGE_COOKIE_SAMESITE = None
161
162
163# If you set this to True, Django will format dates, numbers and calendars
164# according to user current locale.
165USE_L10N = False
166
167# Not-necessarily-technical managers of the site. They get broken link
168# notifications and other various emails.
169MANAGERS = ADMINS
170
171# Default charset to use for all HttpResponse objects, if a MIME type isn't
172# manually specified. It's used to construct the Content-Type header.
173DEFAULT_CHARSET = "utf-8"
174
175# Email address that error messages come from.
176SERVER_EMAIL = "root@localhost"
177
178# Database connection info. If left empty, will default to the dummy backend.
179DATABASES = {}
180
181# Classes used to implement DB routing behavior.
182DATABASE_ROUTERS = []
183
184# The email backend to use. For possible shortcuts see django.core.mail.
185# The default is to use the SMTP backend.
186# Third-party backends can be specified by providing a Python path
187# to a module that defines an EmailBackend class.
188EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
189
190# Host for sending email.
191EMAIL_HOST = "localhost"
192
193# Port for sending email.
194EMAIL_PORT = 25
195
196# Whether to send SMTP 'Date' header in the local time zone or in UTC.
197EMAIL_USE_LOCALTIME = False
198
199# Optional SMTP authentication information for EMAIL_HOST.
200EMAIL_HOST_USER = ""
201EMAIL_HOST_PASSWORD = ""
202EMAIL_USE_TLS = False
203EMAIL_USE_SSL = False
204EMAIL_SSL_CERTFILE = None
205EMAIL_SSL_KEYFILE = None
206EMAIL_TIMEOUT = None
207
208# List of strings representing installed apps.
209INSTALLED_APPS = []
210
211TEMPLATES = []
212
213# Default form rendering class.
214FORM_RENDERER = "django.forms.renderers.DjangoTemplates"
215
216# Default email address to use for various automated correspondence from
217# the site managers.
218DEFAULT_FROM_EMAIL = "webmaster@localhost"
219
220# Subject-line prefix for email messages send with django.core.mail.mail_admins
221# or ...mail_managers.  Make sure to include the trailing space.
222EMAIL_SUBJECT_PREFIX = "[Django] "
223
224# Whether to append trailing slashes to URLs.
225APPEND_SLASH = True
226
227# Whether to prepend the "www." subdomain to URLs that don't have it.
228PREPEND_WWW = False
229
230# Override the server-derived value of SCRIPT_NAME
231FORCE_SCRIPT_NAME = None
232
233# List of compiled regular expression objects representing User-Agent strings
234# that are not allowed to visit any page, systemwide. Use this for bad
235# robots/crawlers. Here are a few examples:
236#     import re
237#     DISALLOWED_USER_AGENTS = [
238#         re.compile(r'^NaverBot.*'),
239#         re.compile(r'^EmailSiphon.*'),
240#         re.compile(r'^SiteSucker.*'),
241#         re.compile(r'^sohu-search'),
242#     ]
243DISALLOWED_USER_AGENTS = []
244
245ABSOLUTE_URL_OVERRIDES = {}
246
247# List of compiled regular expression objects representing URLs that need not
248# be reported by BrokenLinkEmailsMiddleware. Here are a few examples:
249#    import re
250#    IGNORABLE_404_URLS = [
251#        re.compile(r'^/apple-touch-icon.*\.png$'),
252#        re.compile(r'^/favicon.ico$'),
253#        re.compile(r'^/robots.txt$'),
254#        re.compile(r'^/phpmyadmin/'),
255#        re.compile(r'\.(cgi|php|pl)$'),
256#    ]
257IGNORABLE_404_URLS = []
258
259# A secret key for this particular Django installation. Used in secret-key
260# hashing algorithms. Set this in your settings, or Django will complain
261# loudly.
262SECRET_KEY = ""
263
264# Default file storage mechanism that holds media.
265DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
266
267# Absolute filesystem path to the directory that will hold user-uploaded files.
268# Example: "/var/www/example.com/media/"
269MEDIA_ROOT = ""
270
271# URL that handles the media served from MEDIA_ROOT.
272# Examples: "http://example.com/media/", "http://media.example.com/"
273MEDIA_URL = ""
274
275# Absolute path to the directory static files should be collected to.
276# Example: "/var/www/example.com/static/"
277STATIC_ROOT = None
278
279# URL that handles the static files served from STATIC_ROOT.
280# Example: "http://example.com/static/", "http://static.example.com/"
281STATIC_URL = None
282
283# List of upload handler classes to be applied in order.
284FILE_UPLOAD_HANDLERS = [
285    "django.core.files.uploadhandler.MemoryFileUploadHandler",
286    "django.core.files.uploadhandler.TemporaryFileUploadHandler",
287]
288
289# Maximum size, in bytes, of a request before it will be streamed to the
290# file system instead of into memory.
291FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
292
293# Maximum size in bytes of request data (excluding file uploads) that will be
294# read before a SuspiciousOperation (RequestDataTooBig) is raised.
295DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
296
297# Maximum number of GET/POST parameters that will be read before a
298# SuspiciousOperation (TooManyFieldsSent) is raised.
299DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
300
301# Directory in which upload streamed files will be temporarily saved. A value of
302# `None` will make Django use the operating system's default temporary directory
303# (i.e. "/tmp" on *nix systems).
304FILE_UPLOAD_TEMP_DIR = None
305
306# The numeric mode to set newly-uploaded files to. The value should be a mode
307# you'd pass directly to os.chmod; see https://docs.python.org/library/os.html#files-and-directories.
308FILE_UPLOAD_PERMISSIONS = 0o644
309
310# The numeric mode to assign to newly-created directories, when uploading files.
311# The value should be a mode as you'd pass to os.chmod;
312# see https://docs.python.org/library/os.html#files-and-directories.
313FILE_UPLOAD_DIRECTORY_PERMISSIONS = None
314
315# Python module path where user will place custom format definition.
316# The directory where this setting is pointing should contain subdirectories
317# named as the locales, containing a formats.py file
318# (i.e. "myproject.locale" for myproject/locale/en/formats.py etc. use)
319FORMAT_MODULE_PATH = None
320
321# Default formatting for date objects. See all available format strings here:
322# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
323DATE_FORMAT = "N j, Y"
324
325# Default formatting for datetime objects. See all available format strings here:
326# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
327DATETIME_FORMAT = "N j, Y, P"
328
329# Default formatting for time objects. See all available format strings here:
330# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
331TIME_FORMAT = "P"
332
333# Default formatting for date objects when only the year and month are relevant.
334# See all available format strings here:
335# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
336YEAR_MONTH_FORMAT = "F Y"
337
338# Default formatting for date objects when only the month and day are relevant.
339# See all available format strings here:
340# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
341MONTH_DAY_FORMAT = "F j"
342
343# Default short formatting for date objects. See all available format strings here:
344# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
345SHORT_DATE_FORMAT = "m/d/Y"
346
347# Default short formatting for datetime objects.
348# See all available format strings here:
349# https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date
350SHORT_DATETIME_FORMAT = "m/d/Y P"
351
352# Default formats to be used when parsing dates from input boxes, in order
353# See all available format string here:
354# https://docs.python.org/library/datetime.html#strftime-behavior
355# * Note that these format strings are different from the ones to display dates
356DATE_INPUT_FORMATS = [
357    "%Y-%m-%d",
358    "%m/%d/%Y",
359    "%m/%d/%y",  # '2006-10-25', '10/25/2006', '10/25/06'
360    "%b %d %Y",
361    "%b %d, %Y",  # 'Oct 25 2006', 'Oct 25, 2006'
362    "%d %b %Y",
363    "%d %b, %Y",  # '25 Oct 2006', '25 Oct, 2006'
364    "%B %d %Y",
365    "%B %d, %Y",  # 'October 25 2006', 'October 25, 2006'
366    "%d %B %Y",
367    "%d %B, %Y",  # '25 October 2006', '25 October, 2006'
368]
369
370# Default formats to be used when parsing times from input boxes, in order
371# See all available format string here:
372# https://docs.python.org/library/datetime.html#strftime-behavior
373# * Note that these format strings are different from the ones to display dates
374TIME_INPUT_FORMATS = [
375    "%H:%M:%S",  # '14:30:59'
376    "%H:%M:%S.%f",  # '14:30:59.000200'
377    "%H:%M",  # '14:30'
378]
379
380# Default formats to be used when parsing dates and times from input boxes,
381# in order
382# See all available format string here:
383# https://docs.python.org/library/datetime.html#strftime-behavior
384# * Note that these format strings are different from the ones to display dates
385DATETIME_INPUT_FORMATS = [
386    "%Y-%m-%d %H:%M:%S",  # '2006-10-25 14:30:59'
387    "%Y-%m-%d %H:%M:%S.%f",  # '2006-10-25 14:30:59.000200'
388    "%Y-%m-%d %H:%M",  # '2006-10-25 14:30'
389    "%Y-%m-%d",  # '2006-10-25'
390    "%m/%d/%Y %H:%M:%S",  # '10/25/2006 14:30:59'
391    "%m/%d/%Y %H:%M:%S.%f",  # '10/25/2006 14:30:59.000200'
392    "%m/%d/%Y %H:%M",  # '10/25/2006 14:30'
393    "%m/%d/%Y",  # '10/25/2006'
394    "%m/%d/%y %H:%M:%S",  # '10/25/06 14:30:59'
395    "%m/%d/%y %H:%M:%S.%f",  # '10/25/06 14:30:59.000200'
396    "%m/%d/%y %H:%M",  # '10/25/06 14:30'
397    "%m/%d/%y",  # '10/25/06'
398]
399
400# First day of week, to be used on calendars
401# 0 means Sunday, 1 means Monday...
402FIRST_DAY_OF_WEEK = 0
403
404# Decimal separator symbol
405DECIMAL_SEPARATOR = "."
406
407# Boolean that sets whether to add thousand separator when formatting numbers
408USE_THOUSAND_SEPARATOR = False
409
410# Number of digits that will be together, when splitting them by
411# THOUSAND_SEPARATOR. 0 means no grouping, 3 means splitting by thousands...
412NUMBER_GROUPING = 0
413
414# Thousand separator symbol
415THOUSAND_SEPARATOR = ","
416
417# The tablespaces to use for each model when not specified otherwise.
418DEFAULT_TABLESPACE = ""
419DEFAULT_INDEX_TABLESPACE = ""
420
421# Default X-Frame-Options header value
422X_FRAME_OPTIONS = "DENY"
423
424USE_X_FORWARDED_HOST = False
425USE_X_FORWARDED_PORT = False
426
427# The Python dotted path to the WSGI application that Django's internal server
428# (runserver) will use. If `None`, the return value of
429# 'django.core.wsgi.get_wsgi_application' is used, thus preserving the same
430# behavior as previous versions of Django. Otherwise this should point to an
431# actual WSGI application object.
432WSGI_APPLICATION = None
433
434# If your Django app is behind a proxy that sets a header to specify secure
435# connections, AND that proxy ensures that user-submitted headers with the
436# same name are ignored (so that people can't spoof it), set this value to
437# a tuple of (header_name, header_value). For any requests that come in with
438# that header/value, request.is_secure() will return True.
439# WARNING! Only set this if you fully understand what you're doing. Otherwise,
440# you may be opening yourself up to a security risk.
441SECURE_PROXY_SSL_HEADER = None
442
443##############
444# MIDDLEWARE #
445##############
446
447# List of middleware to use. Order is important; in the request phase, these
448# middleware will be applied in the order given, and in the response
449# phase the middleware will be applied in reverse order.
450MIDDLEWARE = []
451
452############
453# SESSIONS #
454############
455
456# Cache to store session data if using the cache session backend.
457SESSION_CACHE_ALIAS = "default"
458# Cookie name. This can be whatever you want.
459SESSION_COOKIE_NAME = "sessionid"
460# Age of cookie, in seconds (default: 2 weeks).
461SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2
462# A string like "example.com", or None for standard domain cookie.
463SESSION_COOKIE_DOMAIN = None
464# Whether the session cookie should be secure (https:// only).
465SESSION_COOKIE_SECURE = False
466# The path of the session cookie.
467SESSION_COOKIE_PATH = "/"
468# Whether to use the HttpOnly flag.
469SESSION_COOKIE_HTTPONLY = True
470# Whether to set the flag restricting cookie leaks on cross-site requests.
471# This can be 'Lax', 'Strict', or None to disable the flag.
472SESSION_COOKIE_SAMESITE = "Lax"
473# Whether to save the session data on every request.
474SESSION_SAVE_EVERY_REQUEST = False
475# Whether a user's session cookie expires when the Web browser is closed.
476SESSION_EXPIRE_AT_BROWSER_CLOSE = False
477# The module to store session data
478SESSION_ENGINE = "django.contrib.sessions.backends.db"
479# Directory to store session files if using the file session module. If None,
480# the backend will use a sensible default.
481SESSION_FILE_PATH = None
482# class to serialize session data
483SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
484
485#########
486# CACHE #
487#########
488
489# The cache backends to use.
490CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache",}}
491CACHE_MIDDLEWARE_KEY_PREFIX = ""
492CACHE_MIDDLEWARE_SECONDS = 600
493CACHE_MIDDLEWARE_ALIAS = "default"
494
495##################
496# AUTHENTICATION #
497##################
498
499AUTH_USER_MODEL = "auth.User"
500
501AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"]
502
503LOGIN_URL = "/accounts/login/"
504
505LOGIN_REDIRECT_URL = "/accounts/profile/"
506
507LOGOUT_REDIRECT_URL = None
508
509# The number of days a password reset link is valid for
510PASSWORD_RESET_TIMEOUT_DAYS = 3
511
512# The minimum number of seconds a password reset link is valid for
513# (default: 3 days).
514PASSWORD_RESET_TIMEOUT = 60 * 60 * 24 * 3
515
516# the first hasher in this list is the preferred algorithm.  any
517# password using different algorithms will be converted automatically
518# upon login
519PASSWORD_HASHERS = [
520    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
521    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
522    "django.contrib.auth.hashers.Argon2PasswordHasher",
523    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
524]
525
526AUTH_PASSWORD_VALIDATORS = []
527
528###########
529# SIGNING #
530###########
531
532SIGNING_BACKEND = "django.core.signing.TimestampSigner"
533
534########
535# CSRF #
536########
537
538# Dotted path to callable to be used as view when a request is
539# rejected by the CSRF middleware.
540CSRF_FAILURE_VIEW = "django.views.csrf.csrf_failure"
541
542# Settings for CSRF cookie.
543CSRF_COOKIE_NAME = "csrftoken"
544CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52
545CSRF_COOKIE_DOMAIN = None
546CSRF_COOKIE_PATH = "/"
547CSRF_COOKIE_SECURE = False
548CSRF_COOKIE_HTTPONLY = False
549CSRF_COOKIE_SAMESITE = "Lax"
550CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
551CSRF_TRUSTED_ORIGINS = []
552CSRF_USE_SESSIONS = False
553
554############
555# MESSAGES #
556############
557
558# Class to use as messages backend
559MESSAGE_STORAGE = "django.contrib.messages.storage.fallback.FallbackStorage"
560
561# Default values of MESSAGE_LEVEL and MESSAGE_TAGS are defined within
562# django.contrib.messages to avoid imports in this settings file.
563
564###########
565# LOGGING #
566###########
567
568# The callable to use to configure logging
569LOGGING_CONFIG = "logging.config.dictConfig"
570
571# Custom logging configuration.
572LOGGING = {}
573
574# Default exception reporter filter class used in case none has been
575# specifically assigned to the HttpRequest instance.
576DEFAULT_EXCEPTION_REPORTER_FILTER = "django.views.debug.SafeExceptionReporterFilter"
577
578###########
579# TESTING #
580###########
581
582# The name of the class to use to run the test suite
583TEST_RUNNER = "django.test.runner.DiscoverRunner"
584
585# Apps that don't need to be serialized at test database creation time
586# (only apps with migrations are to start with)
587TEST_NON_SERIALIZED_APPS = []
588
589############
590# FIXTURES #
591############
592
593# The list of directories to search for fixtures
594FIXTURE_DIRS = []
595
596###############
597# STATICFILES #
598###############
599
600# A list of locations of additional static files
601STATICFILES_DIRS = []
602
603# The default file storage backend used during the build process
604STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
605
606# List of finder classes that know how to find static files in
607# various locations.
608STATICFILES_FINDERS = [
609    "django.contrib.staticfiles.finders.FileSystemFinder",
610    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
611    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
612]
613
614##############
615# MIGRATIONS #
616##############
617
618# Migration module overrides for apps, by app label.
619MIGRATION_MODULES = {}
620
621#################
622# SYSTEM CHECKS #
623#################
624
625# List of all issues generated by system checks that should be silenced. Light
626# issues like warnings, infos or debugs will not generate a message. Silencing
627# serious issues like errors and criticals does not result in hiding the
628# message, but Django will not stop you from e.g. running server.
629SILENCED_SYSTEM_CHECKS = []
630
631#######################
632# SECURITY MIDDLEWARE #
633#######################
634SECURE_BROWSER_XSS_FILTER = False
635SECURE_CONTENT_TYPE_NOSNIFF = True
636SECURE_HSTS_INCLUDE_SUBDOMAINS = False
637SECURE_HSTS_PRELOAD = False
638SECURE_HSTS_SECONDS = 0
639SECURE_REDIRECT_EXEMPT = []
640SECURE_REFERRER_POLICY = None
641SECURE_SSL_HOST = None
642SECURE_SSL_REDIRECT = False

django/contrib/auth/backends.py

  1from django.contrib.auth import get_user_model
  2from django.contrib.auth.models import Permission
  3from django.db.models import Exists, OuterRef, Q
  4
  5UserModel = get_user_model()
  6
  7
  8class BaseBackend:
  9    def authenticate(self, request, **kwargs):
 10        return None
 11
 12    def get_user(self, user_id):
 13        return None
 14
 15    def get_user_permissions(self, user_obj, obj=None):
 16        return set()
 17
 18    def get_group_permissions(self, user_obj, obj=None):
 19        return set()
 20
 21    def get_all_permissions(self, user_obj, obj=None):
 22        return {
 23            *self.get_user_permissions(user_obj, obj=obj),
 24            *self.get_group_permissions(user_obj, obj=obj),
 25        }
 26
 27    def has_perm(self, user_obj, perm, obj=None):
 28        return perm in self.get_all_permissions(user_obj, obj=obj)
 29
 30
 31class ModelBackend(BaseBackend):
 32    """
 33    Authenticates against settings.AUTH_USER_MODEL.
 34    """
 35
 36    def authenticate(self, request, username=None, password=None, **kwargs):
 37        if username is None:
 38            username = kwargs.get(UserModel.USERNAME_FIELD)
 39        if username is None or password is None:
 40            return
 41        try:
 42            user = UserModel._default_manager.get_by_natural_key(username)
 43        except UserModel.DoesNotExist:
 44            # Run the default password hasher once to reduce the timing
 45            # difference between an existing and a nonexistent user (#20760).
 46            UserModel().set_password(password)
 47        else:
 48            if user.check_password(password) and self.user_can_authenticate(user):
 49                return user
 50
 51    def user_can_authenticate(self, user):
 52        """
 53        Reject users with is_active=False. Custom user models that don't have
 54        that attribute are allowed.
 55        """
 56        is_active = getattr(user, "is_active", None)
 57        return is_active or is_active is None
 58
 59    def _get_user_permissions(self, user_obj):
 60        return user_obj.user_permissions.all()
 61
 62    def _get_group_permissions(self, user_obj):
 63        user_groups_field = get_user_model()._meta.get_field("groups")
 64        user_groups_query = "group__%s" % user_groups_field.related_query_name()
 65        return Permission.objects.filter(**{user_groups_query: user_obj})
 66
 67    def _get_permissions(self, user_obj, obj, from_name):
 68        """
 69        Return the permissions of `user_obj` from `from_name`. `from_name` can
 70        be either "group" or "user" to return permissions from
 71        `_get_group_permissions` or `_get_user_permissions` respectively.
 72        """
 73        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
 74            return set()
 75
 76        perm_cache_name = "_%s_perm_cache" % from_name
 77        if not hasattr(user_obj, perm_cache_name):
 78            if user_obj.is_superuser:
 79                perms = Permission.objects.all()
 80            else:
 81                perms = getattr(self, "_get_%s_permissions" % from_name)(user_obj)
 82            perms = perms.values_list("content_type__app_label", "codename").order_by()
 83            setattr(
 84                user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms}
 85            )
 86        return getattr(user_obj, perm_cache_name)
 87
 88    def get_user_permissions(self, user_obj, obj=None):
 89        """
 90        Return a set of permission strings the user `user_obj` has from their
 91        `user_permissions`.
 92        """
 93        return self._get_permissions(user_obj, obj, "user")
 94
 95    def get_group_permissions(self, user_obj, obj=None):
 96        """
 97        Return a set of permission strings the user `user_obj` has from the
 98        groups they belong.
 99        """
100        return self._get_permissions(user_obj, obj, "group")
101
102    def get_all_permissions(self, user_obj, obj=None):
103        if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
104            return set()
105        if not hasattr(user_obj, "_perm_cache"):
106            user_obj._perm_cache = super().get_all_permissions(user_obj)
107        return user_obj._perm_cache
108
109    def has_perm(self, user_obj, perm, obj=None):
110        return user_obj.is_active and super().has_perm(user_obj, perm, obj=obj)
111
112    def has_module_perms(self, user_obj, app_label):
113        """
114        Return True if user_obj has any permissions in the given app_label.
115        """
116        return user_obj.is_active and any(
117            perm[: perm.index(".")] == app_label
118            for perm in self.get_all_permissions(user_obj)
119        )
120
121    def with_perm(self, perm, is_active=True, include_superusers=True, obj=None):
122        """
123        Return users that have permission "perm". By default, filter out
124        inactive users and include superusers.
125        """
126        if isinstance(perm, str):
127            try:
128                app_label, codename = perm.split(".")
129            except ValueError:
130                raise ValueError(
131                    "Permission name should be in the form "
132                    "app_label.permission_codename."
133                )
134        elif not isinstance(perm, Permission):
135            raise TypeError(
136                "The `perm` argument must be a string or a permission instance."
137            )
138
139        UserModel = get_user_model()
140        if obj is not None:
141            return UserModel._default_manager.none()
142
143        permission_q = Q(group__user=OuterRef("pk")) | Q(user=OuterRef("pk"))
144        if isinstance(perm, Permission):
145            permission_q &= Q(pk=perm.pk)
146        else:
147            permission_q &= Q(codename=codename, content_type__app_label=app_label)
148
149        user_q = Exists(Permission.objects.filter(permission_q))
150        if include_superusers:
151            user_q |= Q(is_superuser=True)
152        if is_active is not None:
153            user_q &= Q(is_active=is_active)
154
155        return UserModel._default_manager.filter(user_q)
156
157    def get_user(self, user_id):
158        try:
159            user = UserModel._default_manager.get(pk=user_id)
160        except UserModel.DoesNotExist:
161            return None
162        return user if self.user_can_authenticate(user) else None
163
164
165class AllowAllUsersModelBackend(ModelBackend):
166    def user_can_authenticate(self, user):
167        return True
168
169
170class RemoteUserBackend(ModelBackend):
171    """
172    This backend is to be used in conjunction with the ``RemoteUserMiddleware``
173    found in the middleware module of this package, and is used when the server
174    is handling authentication outside of Django.
175
176    By default, the ``authenticate`` method creates ``User`` objects for
177    usernames that don't already exist in the database.  Subclasses can disable
178    this behavior by setting the ``create_unknown_user`` attribute to
179    ``False``.
180    """
181
182    # Create a User object if not already in the database?
183    create_unknown_user = True
184
185    def authenticate(self, request, remote_user):
186        """
187        The username passed as ``remote_user`` is considered trusted. Return
188        the ``User`` object with the given username. Create a new ``User``
189        object if ``create_unknown_user`` is ``True``.
190
191        Return None if ``create_unknown_user`` is ``False`` and a ``User``
192        object with the given username is not found in the database.
193        """
194        if not remote_user:
195            return
196        user = None
197        username = self.clean_username(remote_user)
198
199        # Note that this could be accomplished in one try-except clause, but
200        # instead we use get_or_create when creating unknown users since it has
201        # built-in safeguards for multiple threads.
202        if self.create_unknown_user:
203            user, created = UserModel._default_manager.get_or_create(
204                **{UserModel.USERNAME_FIELD: username}
205            )
206            if created:
207                user = self.configure_user(request, user)
208        else:
209            try:
210                user = UserModel._default_manager.get_by_natural_key(username)
211            except UserModel.DoesNotExist:
212                pass
213        return user if self.user_can_authenticate(user) else None
214
215    def clean_username(self, username):
216        """
217        Perform any cleaning on the "username" prior to using it to get or
218        create the user object.  Return the cleaned username.
219
220        By default, return the username unchanged.
221        """
222        return username
223
224    def configure_user(self, request, user):
225        """
226        Configure a user after creation and return the updated user.
227
228        By default, return the user unmodified.
229        """
230        return user
231
232
233class AllowAllUsersRemoteUserBackend(RemoteUserBackend):
234    def user_can_authenticate(self, user):
235        return True

django/contrib/auth/base_user.py

  1"""
  2This module allows importing AbstractBaseUser even when django.contrib.auth is
  3not in INSTALLED_APPS.
  4"""
  5import unicodedata
  6
  7from django.contrib.auth import password_validation
  8from django.contrib.auth.hashers import (
  9    check_password,
 10    is_password_usable,
 11    make_password,
 12)
 13from django.db import models
 14from django.utils.crypto import get_random_string, salted_hmac
 15from django.utils.translation import gettext_lazy as _
 16
 17
 18class BaseUserManager(models.Manager):
 19    @classmethod
 20    def normalize_email(cls, email):
 21        """
 22        Normalize the email address by lowercasing the domain part of it.
 23        """
 24        email = email or ""
 25        try:
 26            email_name, domain_part = email.strip().rsplit("@", 1)
 27        except ValueError:
 28            pass
 29        else:
 30            email = email_name + "@" + domain_part.lower()
 31        return email
 32
 33    def make_random_password(
 34        self,
 35        length=10,
 36        allowed_chars="abcdefghjkmnpqrstuvwxyz" "ABCDEFGHJKLMNPQRSTUVWXYZ" "23456789",
 37    ):
 38        """
 39        Generate a random password with the given length and given
 40        allowed_chars. The default value of allowed_chars does not have "I" or
 41        "O" or letters and digits that look similar -- just to avoid confusion.
 42        """
 43        return get_random_string(length, allowed_chars)
 44
 45    def get_by_natural_key(self, username):
 46        return self.get(**{self.model.USERNAME_FIELD: username})
 47
 48
 49class AbstractBaseUser(models.Model):
 50    password = models.CharField(_("password"), max_length=128)
 51    last_login = models.DateTimeField(_("last login"), blank=True, null=True)
 52
 53    is_active = True
 54
 55    REQUIRED_FIELDS = []
 56
 57    # Stores the raw password if set_password() is called so that it can
 58    # be passed to password_changed() after the model is saved.
 59    _password = None
 60
 61    class Meta:
 62        abstract = True
 63
 64    def __str__(self):
 65        return self.get_username()
 66
 67    def save(self, *args, **kwargs):
 68        super().save(*args, **kwargs)
 69        if self._password is not None:
 70            password_validation.password_changed(self._password, self)
 71            self._password = None
 72
 73    def get_username(self):
 74        """Return the username for this User."""
 75        return getattr(self, self.USERNAME_FIELD)
 76
 77    def clean(self):
 78        setattr(self, self.USERNAME_FIELD, self.normalize_username(self.get_username()))
 79
 80    def natural_key(self):
 81        return (self.get_username(),)
 82
 83    @property
 84    def is_anonymous(self):
 85        """
 86        Always return False. This is a way of comparing User objects to
 87        anonymous users.
 88        """
 89        return False
 90
 91    @property
 92    def is_authenticated(self):
 93        """
 94        Always return True. This is a way to tell if the user has been
 95        authenticated in templates.
 96        """
 97        return True
 98
 99    def set_password(self, raw_password):
100        self.password = make_password(raw_password)
101        self._password = raw_password
102
103    def check_password(self, raw_password):
104        """
105        Return a boolean of whether the raw_password was correct. Handles
106        hashing formats behind the scenes.
107        """
108
109        def setter(raw_password):
110            self.set_password(raw_password)
111            # Password hash upgrades shouldn't be considered password changes.
112            self._password = None
113            self.save(update_fields=["password"])
114
115        return check_password(raw_password, self.password, setter)
116
117    def set_unusable_password(self):
118        # Set a value that will never be a valid hash
119        self.password = make_password(None)
120
121    def has_usable_password(self):
122        """
123        Return False if set_unusable_password() has been called for this user.
124        """
125        return is_password_usable(self.password)
126
127    def get_session_auth_hash(self):
128        """
129        Return an HMAC of the password field.
130        """
131        key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
132        return salted_hmac(key_salt, self.password).hexdigest()
133
134    @classmethod
135    def get_email_field_name(cls):
136        try:
137            return cls.EMAIL_FIELD
138        except AttributeError:
139            return "email"
140
141    @classmethod
142    def normalize_username(cls, username):
143        return (
144            unicodedata.normalize("NFKC", username)
145            if isinstance(username, str)
146            else username
147        )

django/contrib/auth/views.py

  1from urllib.parse import urlparse, urlunparse
  2
  3from django.conf import settings
  4
  5# Avoid shadowing the login() and logout() views below.
  6from django.contrib.auth import (
  7    REDIRECT_FIELD_NAME,
  8    get_user_model,
  9    login as auth_login,
 10    logout as auth_logout,
 11    update_session_auth_hash,
 12)
 13from django.contrib.auth.decorators import login_required
 14from django.contrib.auth.forms import (
 15    AuthenticationForm,
 16    PasswordChangeForm,
 17    PasswordResetForm,
 18    SetPasswordForm,
 19)
 20from django.contrib.auth.tokens import default_token_generator
 21from django.contrib.sites.shortcuts import get_current_site
 22from django.core.exceptions import ValidationError
 23from django.http import HttpResponseRedirect, QueryDict
 24from django.shortcuts import resolve_url
 25from django.urls import reverse_lazy
 26from django.utils.decorators import method_decorator
 27from django.utils.http import (
 28    url_has_allowed_host_and_scheme,
 29    urlsafe_base64_decode,
 30)
 31from django.utils.translation import gettext_lazy as _
 32from django.views.decorators.cache import never_cache
 33from django.views.decorators.csrf import csrf_protect
 34from django.views.decorators.debug import sensitive_post_parameters
 35from django.views.generic.base import TemplateView
 36from django.views.generic.edit import FormView
 37
 38UserModel = get_user_model()
 39
 40
 41class SuccessURLAllowedHostsMixin:
 42    success_url_allowed_hosts = set()
 43
 44    def get_success_url_allowed_hosts(self):
 45        return {self.request.get_host(), *self.success_url_allowed_hosts}
 46
 47
 48class LoginView(SuccessURLAllowedHostsMixin, FormView):
 49    """
 50    Display the login form and handle the login action.
 51    """
 52
 53    form_class = AuthenticationForm
 54    authentication_form = None
 55    redirect_field_name = REDIRECT_FIELD_NAME
 56    template_name = "registration/login.html"
 57    redirect_authenticated_user = False
 58    extra_context = None
 59
 60    @method_decorator(sensitive_post_parameters())
 61    @method_decorator(csrf_protect)
 62    @method_decorator(never_cache)
 63    def dispatch(self, request, *args, **kwargs):
 64        if self.redirect_authenticated_user and self.request.user.is_authenticated:
 65            redirect_to = self.get_success_url()
 66            if redirect_to == self.request.path:
 67                raise ValueError(
 68                    "Redirection loop for authenticated user detected. Check that "
 69                    "your LOGIN_REDIRECT_URL doesn't point to a login page."
 70                )
 71            return HttpResponseRedirect(redirect_to)
 72        return super().dispatch(request, *args, **kwargs)
 73
 74    def get_success_url(self):
 75        url = self.get_redirect_url()
 76        return url or resolve_url(settings.LOGIN_REDIRECT_URL)
 77
 78    def get_redirect_url(self):
 79        """Return the user-originating redirect URL if it's safe."""
 80        redirect_to = self.request.POST.get(
 81            self.redirect_field_name, self.request.GET.get(self.redirect_field_name, "")
 82        )
 83        url_is_safe = url_has_allowed_host_and_scheme(
 84            url=redirect_to,
 85            allowed_hosts=self.get_success_url_allowed_hosts(),
 86            require_https=self.request.is_secure(),
 87        )
 88        return redirect_to if url_is_safe else ""
 89
 90    def get_form_class(self):
 91        return self.authentication_form or self.form_class
 92
 93    def get_form_kwargs(self):
 94        kwargs = super().get_form_kwargs()
 95        kwargs["request"] = self.request
 96        return kwargs
 97
 98    def form_valid(self, form):
 99        """Security check complete. Log the user in."""
100        auth_login(self.request, form.get_user())
101        return HttpResponseRedirect(self.get_success_url())
102
103    def get_context_data(self, **kwargs):
104        context = super().get_context_data(**kwargs)
105        current_site = get_current_site(self.request)
106        context.update(
107            {
108                self.redirect_field_name: self.get_redirect_url(),
109                "site": current_site,
110                "site_name": current_site.name,
111                **(self.extra_context or {}),
112            }
113        )
114        return context
115
116
117class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
118    """
119    Log out the user and display the 'You are logged out' message.
120    """
121
122    next_page = None
123    redirect_field_name = REDIRECT_FIELD_NAME
124    template_name = "registration/logged_out.html"
125    extra_context = None
126
127    @method_decorator(never_cache)
128    def dispatch(self, request, *args, **kwargs):
129        auth_logout(request)
130        next_page = self.get_next_page()
131        if next_page:
132            # Redirect to this page until the session has been cleared.
133            return HttpResponseRedirect(next_page)
134        return super().dispatch(request, *args, **kwargs)
135
136    def post(self, request, *args, **kwargs):
137        """Logout may be done via POST."""
138        return self.get(request, *args, **kwargs)
139
140    def get_next_page(self):
141        if self.next_page is not None:
142            next_page = resolve_url(self.next_page)
143        elif settings.LOGOUT_REDIRECT_URL:
144            next_page = resolve_url(settings.LOGOUT_REDIRECT_URL)
145        else:
146            next_page = self.next_page
147
148        if (
149            self.redirect_field_name in self.request.POST
150            or self.redirect_field_name in self.request.GET
151        ):
152            next_page = self.request.POST.get(
153                self.redirect_field_name, self.request.GET.get(self.redirect_field_name)
154            )
155            url_is_safe = url_has_allowed_host_and_scheme(
156                url=next_page,
157                allowed_hosts=self.get_success_url_allowed_hosts(),
158                require_https=self.request.is_secure(),
159            )
160            # Security check -- Ensure the user-originating redirection URL is
161            # safe.
162            if not url_is_safe:
163                next_page = self.request.path
164        return next_page
165
166    def get_context_data(self, **kwargs):
167        context = super().get_context_data(**kwargs)
168        current_site = get_current_site(self.request)
169        context.update(
170            {
171                "site": current_site,
172                "site_name": current_site.name,
173                "title": _("Logged out"),
174                **(self.extra_context or {}),
175            }
176        )
177        return context
178
179
180def logout_then_login(request, login_url=None):
181    """
182    Log out the user if they are logged in. Then redirect to the login page.
183    """
184    login_url = resolve_url(login_url or settings.LOGIN_URL)
185    return LogoutView.as_view(next_page=login_url)(request)
186
187
188def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
189    """
190    Redirect the user to the login page, passing the given 'next' page.
191    """
192    resolved_url = resolve_url(login_url or settings.LOGIN_URL)
193
194    login_url_parts = list(urlparse(resolved_url))
195    if redirect_field_name:
196        querystring = QueryDict(login_url_parts[4], mutable=True)
197        querystring[redirect_field_name] = next
198        login_url_parts[4] = querystring.urlencode(safe="/")
199
200    return HttpResponseRedirect(urlunparse(login_url_parts))
201
202
203# Class-based password reset views
204# - PasswordResetView sends the mail
205# - PasswordResetDoneView shows a success message for the above
206# - PasswordResetConfirmView checks the link the user clicked and
207#   prompts for a new password
208# - PasswordResetCompleteView shows a success message for the above
209
210
211class PasswordContextMixin:
212    extra_context = None
213
214    def get_context_data(self, **kwargs):
215        context = super().get_context_data(**kwargs)
216        context.update({"title": self.title, **(self.extra_context or {})})
217        return context
218
219
220class PasswordResetView(PasswordContextMixin, FormView):
221    email_template_name = "registration/password_reset_email.html"
222    extra_email_context = None
223    form_class = PasswordResetForm
224    from_email = None
225    html_email_template_name = None
226    subject_template_name = "registration/password_reset_subject.txt"
227    success_url = reverse_lazy("password_reset_done")
228    template_name = "registration/password_reset_form.html"
229    title = _("Password reset")
230    token_generator = default_token_generator
231
232    @method_decorator(csrf_protect)
233    def dispatch(self, *args, **kwargs):
234        return super().dispatch(*args, **kwargs)
235
236    def form_valid(self, form):
237        opts = {
238            "use_https": self.request.is_secure(),
239            "token_generator": self.token_generator,
240            "from_email": self.from_email,
241            "email_template_name": self.email_template_name,
242            "subject_template_name": self.subject_template_name,
243            "request": self.request,
244            "html_email_template_name": self.html_email_template_name,
245            "extra_email_context": self.extra_email_context,
246        }
247        form.save(**opts)
248        return super().form_valid(form)
249
250
251INTERNAL_RESET_SESSION_TOKEN = "_password_reset_token"
252
253
254class PasswordResetDoneView(PasswordContextMixin, TemplateView):
255    template_name = "registration/password_reset_done.html"
256    title = _("Password reset sent")
257
258
259class PasswordResetConfirmView(PasswordContextMixin, FormView):
260    form_class = SetPasswordForm
261    post_reset_login = False
262    post_reset_login_backend = None
263    reset_url_token = "set-password"
264    success_url = reverse_lazy("password_reset_complete")
265    template_name = "registration/password_reset_confirm.html"
266    title = _("Enter new password")
267    token_generator = default_token_generator
268
269    @method_decorator(sensitive_post_parameters())
270    @method_decorator(never_cache)
271    def dispatch(self, *args, **kwargs):
272        assert "uidb64" in kwargs and "token" in kwargs
273
274        self.validlink = False
275        self.user = self.get_user(kwargs["uidb64"])
276
277        if self.user is not None:
278            token = kwargs["token"]
279            if token == self.reset_url_token:
280                session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
281                if self.token_generator.check_token(self.user, session_token):
282                    # If the token is valid, display the password reset form.
283                    self.validlink = True
284                    return super().dispatch(*args, **kwargs)
285            else:
286                if self.token_generator.check_token(self.user, token):
287                    # Store the token in the session and redirect to the
288                    # password reset form at a URL without the token. That
289                    # avoids the possibility of leaking the token in the
290                    # HTTP Referer header.
291                    self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
292                    redirect_url = self.request.path.replace(
293                        token, self.reset_url_token
294                    )
295                    return HttpResponseRedirect(redirect_url)
296
297        # Display the "Password reset unsuccessful" page.
298        return self.render_to_response(self.get_context_data())
299
300    def get_user(self, uidb64):
301        try:
302            # urlsafe_base64_decode() decodes to bytestring
303            uid = urlsafe_base64_decode(uidb64).decode()
304            user = UserModel._default_manager.get(pk=uid)
305        except (
306            TypeError,
307            ValueError,
308            OverflowError,
309            UserModel.DoesNotExist,
310            ValidationError,
311        ):
312            user = None
313        return user
314
315    def get_form_kwargs(self):
316        kwargs = super().get_form_kwargs()
317        kwargs["user"] = self.user
318        return kwargs
319
320    def form_valid(self, form):
321        user = form.save()
322        del self.request.session[INTERNAL_RESET_SESSION_TOKEN]
323        if self.post_reset_login:
324            auth_login(self.request, user, self.post_reset_login_backend)
325        return super().form_valid(form)
326
327    def get_context_data(self, **kwargs):
328        context = super().get_context_data(**kwargs)
329        if self.validlink:
330            context["validlink"] = True
331        else:
332            context.update(
333                {
334                    "form": None,
335                    "title": _("Password reset unsuccessful"),
336                    "validlink": False,
337                }
338            )
339        return context
340
341
342class PasswordResetCompleteView(PasswordContextMixin, TemplateView):
343    template_name = "registration/password_reset_complete.html"
344    title = _("Password reset complete")
345
346    def get_context_data(self, **kwargs):
347        context = super().get_context_data(**kwargs)
348        context["login_url"] = resolve_url(settings.LOGIN_URL)
349        return context
350
351
352class PasswordChangeView(PasswordContextMixin, FormView):
353    form_class = PasswordChangeForm
354    success_url = reverse_lazy("password_change_done")
355    template_name = "registration/password_change_form.html"
356    title = _("Password change")
357
358    @method_decorator(sensitive_post_parameters())
359    @method_decorator(csrf_protect)
360    @method_decorator(login_required)
361    def dispatch(self, *args, **kwargs):
362        return super().dispatch(*args, **kwargs)
363
364    def get_form_kwargs(self):
365        kwargs = super().get_form_kwargs()
366        kwargs["user"] = self.request.user
367        return kwargs
368
369    def form_valid(self, form):
370        form.save()
371        # Updating the password logs out all other sessions for the user
372        # except the current one.
373        update_session_auth_hash(self.request, form.user)
374        return super().form_valid(form)
375
376
377class PasswordChangeDoneView(PasswordContextMixin, TemplateView):
378    template_name = "registration/password_change_done.html"
379    title = _("Password change successful")
380
381    @method_decorator(login_required)
382    def dispatch(self, *args, **kwargs):
383        return super().dispatch(*args, **kwargs)

django/contrib/auth/management/commands/createsuperuser.py

  1"""
  2Management utility to create superusers.
  3"""
  4import getpass
  5import os
  6import sys
  7
  8from django.contrib.auth import get_user_model
  9from django.contrib.auth.management import get_default_username
 10from django.contrib.auth.password_validation import validate_password
 11from django.core import exceptions
 12from django.core.management.base import BaseCommand, CommandError
 13from django.db import DEFAULT_DB_ALIAS
 14from django.utils.text import capfirst
 15
 16
 17class NotRunningInTTYException(Exception):
 18    pass
 19
 20
 21PASSWORD_FIELD = "password"
 22
 23
 24class Command(BaseCommand):
 25    help = "Used to create a superuser."
 26    requires_migrations_checks = True
 27    stealth_options = ("stdin",)
 28
 29    def __init__(self, *args, **kwargs):
 30        super().__init__(*args, **kwargs)
 31        self.UserModel = get_user_model()
 32        self.username_field = self.UserModel._meta.get_field(
 33            self.UserModel.USERNAME_FIELD
 34        )
 35
 36    def add_arguments(self, parser):
 37        parser.add_argument(
 38            "--%s" % self.UserModel.USERNAME_FIELD,
 39            help="Specifies the login for the superuser.",
 40        )
 41        parser.add_argument(
 42            "--noinput",
 43            "--no-input",
 44            action="store_false",
 45            dest="interactive",
 46            help=(
 47                "Tells Django to NOT prompt the user for input of any kind. "
 48                "You must use --%s with --noinput, along with an option for "
 49                "any other required field. Superusers created with --noinput will "
 50                "not be able to log in until they're given a valid password."
 51                % self.UserModel.USERNAME_FIELD
 52            ),
 53        )
 54        parser.add_argument(
 55            "--database",
 56            default=DEFAULT_DB_ALIAS,
 57            help='Specifies the database to use. Default is "default".',
 58        )
 59        for field_name in self.UserModel.REQUIRED_FIELDS:
 60            field = self.UserModel._meta.get_field(field_name)
 61            if field.many_to_many:
 62                if (
 63                    field.remote_field.through
 64                    and not field.remote_field.through._meta.auto_created
 65                ):
 66                    raise CommandError(
 67                        "Required field '%s' specifies a many-to-many "
 68                        "relation through model, which is not supported." % field_name
 69                    )
 70                else:
 71                    parser.add_argument(
 72                        "--%s" % field_name,
 73                        action="append",
 74                        help=(
 75                            "Specifies the %s for the superuser. Can be used "
 76                            "multiple times." % field_name,
 77                        ),
 78                    )
 79            else:
 80                parser.add_argument(
 81                    "--%s" % field_name,
 82                    help="Specifies the %s for the superuser." % field_name,
 83                )
 84
 85    def execute(self, *args, **options):
 86        self.stdin = options.get("stdin", sys.stdin)  # Used for testing
 87        return super().execute(*args, **options)
 88
 89    def handle(self, *args, **options):
 90        username = options[self.UserModel.USERNAME_FIELD]
 91        database = options["database"]
 92        user_data = {}
 93        verbose_field_name = self.username_field.verbose_name
 94        try:
 95            self.UserModel._meta.get_field(PASSWORD_FIELD)
 96        except exceptions.FieldDoesNotExist:
 97            pass
 98        else:
 99            # If not provided, create the user with an unusable password.
100            user_data[PASSWORD_FIELD] = None
101        try:
102            if options["interactive"]:
103                # Same as user_data but without many to many fields and with
104                # foreign keys as fake model instances instead of raw IDs.
105                fake_user_data = {}
106                if hasattr(self.stdin, "isatty") and not self.stdin.isatty():
107                    raise NotRunningInTTYException
108                default_username = get_default_username()
109                if username:
110                    error_msg = self._validate_username(
111                        username, verbose_field_name, database
112                    )
113                    if error_msg:
114                        self.stderr.write(error_msg)
115                        username = None
116                elif username == "":
117                    raise CommandError(
118                        "%s cannot be blank." % capfirst(verbose_field_name)
119                    )
120                # Prompt for username.
121                while username is None:
122                    message = self._get_input_message(
123                        self.username_field, default_username
124                    )
125                    username = self.get_input_data(
126                        self.username_field, message, default_username
127                    )
128                    if username:
129                        error_msg = self._validate_username(
130                            username, verbose_field_name, database
131                        )
132                        if error_msg:
133                            self.stderr.write(error_msg)
134                            username = None
135                            continue
136                user_data[self.UserModel.USERNAME_FIELD] = username
137                fake_user_data[self.UserModel.USERNAME_FIELD] = (
138                    self.username_field.remote_field.model(username)
139                    if self.username_field.remote_field
140                    else username
141                )
142                # Prompt for required fields.
143                for field_name in self.UserModel.REQUIRED_FIELDS:
144                    field = self.UserModel._meta.get_field(field_name)
145                    user_data[field_name] = options[field_name]
146                    while user_data[field_name] is None:
147                        message = self._get_input_message(field)
148                        input_value = self.get_input_data(field, message)
149                        user_data[field_name] = input_value
150                        if field.many_to_many and input_value:
151                            if not input_value.strip():
152                                user_data[field_name] = None
153                                self.stderr.write("Error: This field cannot be blank.")
154                                continue
155                            user_data[field_name] = [
156                                pk.strip() for pk in input_value.split(",")
157                            ]
158                        if not field.many_to_many:
159                            fake_user_data[field_name] = input_value
160
161                        # Wrap any foreign keys in fake model instances
162                        if field.many_to_one:
163                            fake_user_data[field_name] = field.remote_field.model(
164                                input_value
165                            )
166
167                # Prompt for a password if the model has one.
168                while PASSWORD_FIELD in user_data and user_data[PASSWORD_FIELD] is None:
169                    password = getpass.getpass()
170                    password2 = getpass.getpass("Password (again): ")
171                    if password != password2:
172                        self.stderr.write("Error: Your passwords didn't match.")
173                        # Don't validate passwords that don't match.
174                        continue
175                    if password.strip() == "":
176                        self.stderr.write("Error: Blank passwords aren't allowed.")
177                        # Don't validate blank passwords.
178                        continue
179                    try:
180                        validate_password(password2, self.UserModel(**fake_user_data))
181                    except exceptions.ValidationError as err:
182                        self.stderr.write("\n".join(err.messages))
183                        response = input(
184                            "Bypass password validation and create user anyway? [y/N]: "
185                        )
186                        if response.lower() != "y":
187                            continue
188                    user_data[PASSWORD_FIELD] = password
189            else:
190                # Non-interactive mode.
191                # Use password from environment variable, if provided.
192                if (
193                    PASSWORD_FIELD in user_data
194                    and "DJANGO_SUPERUSER_PASSWORD" in os.environ
195                ):
196                    user_data[PASSWORD_FIELD] = os.environ["DJANGO_SUPERUSER_PASSWORD"]
197                # Use username from environment variable, if not provided in
198                # options.
199                if username is None:
200                    username = os.environ.get(
201                        "DJANGO_SUPERUSER_" + self.UserModel.USERNAME_FIELD.upper()
202                    )
203                if username is None:
204                    raise CommandError(
205                        "You must use --%s with --noinput."
206                        % self.UserModel.USERNAME_FIELD
207                    )
208                else:
209                    error_msg = self._validate_username(
210                        username, verbose_field_name, database
211                    )
212                    if error_msg:
213                        raise CommandError(error_msg)
214
215                user_data[self.UserModel.USERNAME_FIELD] = username
216                for field_name in self.UserModel.REQUIRED_FIELDS:
217                    env_var = "DJANGO_SUPERUSER_" + field_name.upper()
218                    value = options[field_name] or os.environ.get(env_var)
219                    if not value:
220                        raise CommandError(
221                            "You must use --%s with --noinput." % field_name
222                        )
223                    field = self.UserModel._meta.get_field(field_name)
224                    user_data[field_name] = field.clean(value, None)
225
226            self.UserModel._default_manager.db_manager(database).create_superuser(
227                **user_data
228            )
229            if options["verbosity"] >= 1:
230                self.stdout.write("Superuser created successfully.")
231        except KeyboardInterrupt:
232            self.stderr.write("\nOperation cancelled.")
233            sys.exit(1)
234        except exceptions.ValidationError as e:
235            raise CommandError("; ".join(e.messages))
236        except NotRunningInTTYException:
237            self.stdout.write(
238                "Superuser creation skipped due to not running in a TTY. "
239                "You can run `manage.py createsuperuser` in your project "
240                "to create one manually."
241            )
242
243    def get_input_data(self, field, message, default=None):
244        """
245        Override this method if you want to customize data inputs or
246        validation exceptions.
247        """
248        raw_value = input(message)
249        if default and raw_value == "":
250            raw_value = default
251        try:
252            val = field.clean(raw_value, None)
253        except exceptions.ValidationError as e:
254            self.stderr.write("Error: %s" % "; ".join(e.messages))
255            val = None
256
257        return val
258
259    def _get_input_message(self, field, default=None):
260        return "%s%s%s: " % (
261            capfirst(field.verbose_name),
262            " (leave blank to use '%s')" % default if default else "",
263            " (%s.%s)"
264            % (
265                field.remote_field.model._meta.object_name,
266                field.m2m_target_field_name()
267                if field.many_to_many
268                else field.remote_field.field_name,
269            )
270            if field.remote_field
271            else "",
272        )
273
274    def _validate_username(self, username, verbose_field_name, database):
275        """Validate username. If invalid, return a string error message."""
276        if self.username_field.unique:
277            try:
278                self.UserModel._default_manager.db_manager(database).get_by_natural_key(
279                    username
280                )
281            except self.UserModel.DoesNotExist:
282                pass
283            else:
284                return "Error: That %s is already taken." % verbose_field_name
285        if not username:
286            return "%s cannot be blank." % capfirst(verbose_field_name)
287        try:
288            self.username_field.clean(username, None)
289        except exceptions.ValidationError as e:
290            return "; ".join(e.messages)

django/contrib/admin/views/autocomplete.py

 1from django.http import Http404, JsonResponse
 2from django.views.generic.list import BaseListView
 3
 4
 5class AutocompleteJsonView(BaseListView):
 6    """Handle AutocompleteWidget's AJAX requests for data."""
 7
 8    paginate_by = 20
 9    model_admin = None
10
11    def get(self, request, *args, **kwargs):
12        """
13        Return a JsonResponse with search results of the form:
14        {
15            results: [{id: "123" text: "foo"}],
16            pagination: {more: true}
17        }
18        """
19        if not self.model_admin.get_search_fields(request):
20            raise Http404(
21                "%s must have search_fields for the autocomplete_view."
22                % type(self.model_admin).__name__
23            )
24        if not self.has_perm(request):
25            return JsonResponse({"error": "403 Forbidden"}, status=403)
26
27        self.term = request.GET.get("term", "")
28        self.object_list = self.get_queryset()
29        context = self.get_context_data()
30        return JsonResponse(
31            {
32                "results": [
33                    {"id": str(obj.pk), "text": str(obj)}
34                    for obj in context["object_list"]
35                ],
36                "pagination": {"more": context["page_obj"].has_next()},
37            }
38        )
39
40    def get_paginator(self, *args, **kwargs):
41        """Use the ModelAdmin's paginator."""
42        return self.model_admin.get_paginator(self.request, *args, **kwargs)
43
44    def get_queryset(self):
45        """Return queryset based on ModelAdmin.get_search_results()."""
46        qs = self.model_admin.get_queryset(self.request)
47        qs, search_use_distinct = self.model_admin.get_search_results(
48            self.request, qs, self.term
49        )
50        if search_use_distinct:
51            qs = qs.distinct()
52        return qs
53
54    def has_perm(self, request, obj=None):
55        """Check if user has permission to access the related model."""
56        return self.model_admin.has_view_permission(request, obj=obj)

django/contrib/auth/forms.py

  1import unicodedata
  2
  3from django import forms
  4from django.contrib.auth import (
  5    authenticate,
  6    get_user_model,
  7    password_validation,
  8)
  9from django.contrib.auth.hashers import (
 10    UNUSABLE_PASSWORD_PREFIX,
 11    identify_hasher,
 12)
 13from django.contrib.auth.models import User
 14from django.contrib.auth.tokens import default_token_generator
 15from django.contrib.sites.shortcuts import get_current_site
 16from django.core.mail import EmailMultiAlternatives
 17from django.template import loader
 18from django.utils.encoding import force_bytes
 19from django.utils.http import urlsafe_base64_encode
 20from django.utils.text import capfirst
 21from django.utils.translation import gettext, gettext_lazy as _
 22
 23UserModel = get_user_model()
 24
 25
 26class ReadOnlyPasswordHashWidget(forms.Widget):
 27    template_name = "auth/widgets/read_only_password_hash.html"
 28    read_only = True
 29
 30    def get_context(self, name, value, attrs):
 31        context = super().get_context(name, value, attrs)
 32        summary = []
 33        if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
 34            summary.append({"label": gettext("No password set.")})
 35        else:
 36            try:
 37                hasher = identify_hasher(value)
 38            except ValueError:
 39                summary.append(
 40                    {
 41                        "label": gettext(
 42                            "Invalid password format or unknown hashing algorithm."
 43                        )
 44                    }
 45                )
 46            else:
 47                for key, value_ in hasher.safe_summary(value).items():
 48                    summary.append({"label": gettext(key), "value": value_})
 49        context["summary"] = summary
 50        return context
 51
 52
 53class ReadOnlyPasswordHashField(forms.Field):
 54    widget = ReadOnlyPasswordHashWidget
 55
 56    def __init__(self, *args, **kwargs):
 57        kwargs.setdefault("required", False)
 58        super().__init__(*args, **kwargs)
 59
 60    def bound_data(self, data, initial):
 61        # Always return initial because the widget doesn't
 62        # render an input field.
 63        return initial
 64
 65    def has_changed(self, initial, data):
 66        return False
 67
 68
 69class UsernameField(forms.CharField):
 70    def to_python(self, value):
 71        return unicodedata.normalize("NFKC", super().to_python(value))
 72
 73    def widget_attrs(self, widget):
 74        return {
 75            **super().widget_attrs(widget),
 76            "autocapitalize": "none",
 77            "autocomplete": "username",
 78        }
 79
 80
 81class UserCreationForm(forms.ModelForm):
 82    """
 83    A form that creates a user, with no privileges, from the given username and
 84    password.
 85    """
 86
 87    error_messages = {
 88        "password_mismatch": _("The two password fields didn’t match."),
 89    }
 90    password1 = forms.CharField(
 91        label=_("Password"),
 92        strip=False,
 93        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
 94        help_text=password_validation.password_validators_help_text_html(),
 95    )
 96    password2 = forms.CharField(
 97        label=_("Password confirmation"),
 98        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
 99        strip=False,
100        help_text=_("Enter the same password as before, for verification."),
101    )
102
103    class Meta:
104        model = User
105        fields = ("username",)
106        field_classes = {"username": UsernameField}
107
108    def __init__(self, *args, **kwargs):
109        super().__init__(*args, **kwargs)
110        if self._meta.model.USERNAME_FIELD in self.fields:
111            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs[
112                "autofocus"
113            ] = True
114
115    def clean_password2(self):
116        password1 = self.cleaned_data.get("password1")
117        password2 = self.cleaned_data.get("password2")
118        if password1 and password2 and password1 != password2:
119            raise forms.ValidationError(
120                self.error_messages["password_mismatch"], code="password_mismatch",
121            )
122        return password2
123
124    def _post_clean(self):
125        super()._post_clean()
126        # Validate the password after self.instance is updated with form data
127        # by super().
128        password = self.cleaned_data.get("password2")
129        if password:
130            try:
131                password_validation.validate_password(password, self.instance)
132            except forms.ValidationError as error:
133                self.add_error("password2", error)
134
135    def save(self, commit=True):
136        user = super().save(commit=False)
137        user.set_password(self.cleaned_data["password1"])
138        if commit:
139            user.save()
140        return user
141
142
143class UserChangeForm(forms.ModelForm):
144    password = ReadOnlyPasswordHashField(
145        label=_("Password"),
146        help_text=_(
147            "Raw passwords are not stored, so there is no way to see this "
148            "user’s password, but you can change the password using "
149            '<a href="{}">this form</a>.'
150        ),
151    )
152
153    class Meta:
154        model = User
155        fields = "__all__"
156        field_classes = {"username": UsernameField}
157
158    def __init__(self, *args, **kwargs):
159        super().__init__(*args, **kwargs)
160        password = self.fields.get("password")
161        if password:
162            password.help_text = password.help_text.format("../password/")
163        user_permissions = self.fields.get("user_permissions")
164        if user_permissions:
165            user_permissions.queryset = user_permissions.queryset.select_related(
166                "content_type"
167            )
168
169    def clean_password(self):
170        # Regardless of what the user provides, return the initial value.
171        # This is done here, rather than on the field, because the
172        # field does not have access to the initial value
173        return self.initial.get("password")
174
175
176class AuthenticationForm(forms.Form):
177    """
178    Base class for authenticating users. Extend this to get a form that accepts
179    username/password logins.
180    """
181
182    username = UsernameField(widget=forms.TextInput(attrs={"autofocus": True}))
183    password = forms.CharField(
184        label=_("Password"),
185        strip=False,
186        widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
187    )
188
189    error_messages = {
190        "invalid_login": _(
191            "Please enter a correct %(username)s and password. Note that both "
192            "fields may be case-sensitive."
193        ),
194        "inactive": _("This account is inactive."),
195    }
196
197    def __init__(self, request=None, *args, **kwargs):
198        """
199        The 'request' parameter is set for custom auth use by subclasses.
200        The form data comes in via the standard 'data' kwarg.
201        """
202        self.request = request
203        self.user_cache = None
204        super().__init__(*args, **kwargs)
205
206        # Set the max length and label for the "username" field.
207        self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
208        username_max_length = self.username_field.max_length or 254
209        self.fields["username"].max_length = username_max_length
210        self.fields["username"].widget.attrs["maxlength"] = username_max_length
211        if self.fields["username"].label is None:
212            self.fields["username"].label = capfirst(self.username_field.verbose_name)
213
214    def clean(self):
215        username = self.cleaned_data.get("username")
216        password = self.cleaned_data.get("password")
217
218        if username is not None and password:
219            self.user_cache = authenticate(
220                self.request, username=username, password=password
221            )
222            if self.user_cache is None:
223                raise self.get_invalid_login_error()
224            else:
225                self.confirm_login_allowed(self.user_cache)
226
227        return self.cleaned_data
228
229    def confirm_login_allowed(self, user):
230        """
231        Controls whether the given User may log in. This is a policy setting,
232        independent of end-user authentication. This default behavior is to
233        allow login by active users, and reject login by inactive users.
234
235        If the given user cannot log in, this method should raise a
236        ``forms.ValidationError``.
237
238        If the given user may log in, this method should return None.
239        """
240        if not user.is_active:
241            raise forms.ValidationError(
242                self.error_messages["inactive"], code="inactive",
243            )
244
245    def get_user(self):
246        return self.user_cache
247
248    def get_invalid_login_error(self):
249        return forms.ValidationError(
250            self.error_messages["invalid_login"],
251            code="invalid_login",
252            params={"username": self.username_field.verbose_name},
253        )
254
255
256class PasswordResetForm(forms.Form):
257    email = forms.EmailField(
258        label=_("Email"),
259        max_length=254,
260        widget=forms.EmailInput(attrs={"autocomplete": "email"}),
261    )
262
263    def send_mail(
264        self,
265        subject_template_name,
266        email_template_name,
267        context,
268        from_email,
269        to_email,
270        html_email_template_name=None,
271    ):
272        """
273        Send a django.core.mail.EmailMultiAlternatives to `to_email`.
274        """
275        subject = loader.render_to_string(subject_template_name, context)
276        # Email subject *must not* contain newlines
277        subject = "".join(subject.splitlines())
278        body = loader.render_to_string(email_template_name, context)
279
280        email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
281        if html_email_template_name is not None:
282            html_email = loader.render_to_string(html_email_template_name, context)
283            email_message.attach_alternative(html_email, "text/html")
284
285        email_message.send()
286
287    def get_users(self, email):
288        """Given an email, return matching user(s) who should receive a reset.
289
290        This allows subclasses to more easily customize the default policies
291        that prevent inactive users and users with unusable passwords from
292        resetting their password.
293        """
294        active_users = UserModel._default_manager.filter(
295            **{
296                "%s__iexact" % UserModel.get_email_field_name(): email,
297                "is_active": True,
298            }
299        )
300        return (u for u in active_users if u.has_usable_password())
301
302    def save(
303        self,
304        domain_override=None,
305        subject_template_name="registration/password_reset_subject.txt",
306        email_template_name="registration/password_reset_email.html",
307        use_https=False,
308        token_generator=default_token_generator,
309        from_email=None,
310        request=None,
311        html_email_template_name=None,
312        extra_email_context=None,
313    ):
314        """
315        Generate a one-use only link for resetting password and send it to the
316        user.
317        """
318        email = self.cleaned_data["email"]
319        if not domain_override:
320            current_site = get_current_site(request)
321            site_name = current_site.name
322            domain = current_site.domain
323        else:
324            site_name = domain = domain_override
325        for user in self.get_users(email):
326            context = {
327                "email": email,
328                "domain": domain,
329                "site_name": site_name,
330                "uid": urlsafe_base64_encode(force_bytes(user.pk)),
331                "user": user,
332                "token": token_generator.make_token(user),
333                "protocol": "https" if use_https else "http",
334                **(extra_email_context or {}),
335            }
336            self.send_mail(
337                subject_template_name,
338                email_template_name,
339                context,
340                from_email,
341                email,
342                html_email_template_name=html_email_template_name,
343            )
344
345
346class SetPasswordForm(forms.Form):
347    """
348    A form that lets a user change set their password without entering the old
349    password
350    """
351
352    error_messages = {
353        "password_mismatch": _("The two password fields didn’t match."),
354    }
355    new_password1 = forms.CharField(
356        label=_("New password"),
357        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
358        strip=False,
359        help_text=password_validation.password_validators_help_text_html(),
360    )
361    new_password2 = forms.CharField(
362        label=_("New password confirmation"),
363        strip=False,
364        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
365    )
366
367    def __init__(self, user, *args, **kwargs):
368        self.user = user
369        super().__init__(*args, **kwargs)
370
371    def clean_new_password2(self):
372        password1 = self.cleaned_data.get("new_password1")
373        password2 = self.cleaned_data.get("new_password2")
374        if password1 and password2:
375            if password1 != password2:
376                raise forms.ValidationError(
377                    self.error_messages["password_mismatch"], code="password_mismatch",
378                )
379        password_validation.validate_password(password2, self.user)
380        return password2
381
382    def save(self, commit=True):
383        password = self.cleaned_data["new_password1"]
384        self.user.set_password(password)
385        if commit:
386            self.user.save()
387        return self.user
388
389
390class PasswordChangeForm(SetPasswordForm):
391    """
392    A form that lets a user change their password by entering their old
393    password.
394    """
395
396    error_messages = {
397        **SetPasswordForm.error_messages,
398        "password_incorrect": _(
399            "Your old password was entered incorrectly. Please enter it again."
400        ),
401    }
402    old_password = forms.CharField(
403        label=_("Old password"),
404        strip=False,
405        widget=forms.PasswordInput(
406            attrs={"autocomplete": "current-password", "autofocus": True}
407        ),
408    )
409
410    field_order = ["old_password", "new_password1", "new_password2"]
411
412    def clean_old_password(self):
413        """
414        Validate that the old_password field is correct.
415        """
416        old_password = self.cleaned_data["old_password"]
417        if not self.user.check_password(old_password):
418            raise forms.ValidationError(
419                self.error_messages["password_incorrect"], code="password_incorrect",
420            )
421        return old_password
422
423
424class AdminPasswordChangeForm(forms.Form):
425    """
426    A form used to change the password of a user in the admin interface.
427    """
428
429    error_messages = {
430        "password_mismatch": _("The two password fields didn’t match."),
431    }
432    required_css_class = "required"
433    password1 = forms.CharField(
434        label=_("Password"),
435        widget=forms.PasswordInput(
436            attrs={"autocomplete": "new-password", "autofocus": True}
437        ),
438        strip=False,
439        help_text=password_validation.password_validators_help_text_html(),
440    )
441    password2 = forms.CharField(
442        label=_("Password (again)"),
443        widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
444        strip=False,
445        help_text=_("Enter the same password as before, for verification."),
446    )
447
448    def __init__(self, user, *args, **kwargs):
449        self.user = user
450        super().__init__(*args, **kwargs)
451
452    def clean_password2(self):
453        password1 = self.cleaned_data.get("password1")
454        password2 = self.cleaned_data.get("password2")
455        if password1 and password2:
456            if password1 != password2:
457                raise forms.ValidationError(
458                    self.error_messages["password_mismatch"], code="password_mismatch",
459                )
460        password_validation.validate_password(password2, self.user)
461        return password2
462
463    def save(self, commit=True):
464        """Save the new password."""
465        password = self.cleaned_data["password1"]
466        self.user.set_password(password)
467        if commit:
468            self.user.save()
469        return self.user
470
471    @property
472    def changed_data(self):
473        data = super().changed_data
474        for name in self.fields:
475            if name not in data:
476                return []
477        return ["password"]

django/contrib/admin/checks.py

   1from itertools import chain
   2
   3from django.apps import apps
   4from django.conf import settings
   5from django.contrib.admin.utils import (
   6    NotRelationField,
   7    flatten,
   8    get_fields_from_path,
   9)
  10from django.core import checks
  11from django.core.exceptions import FieldDoesNotExist
  12from django.db import models
  13from django.db.models.constants import LOOKUP_SEP
  14from django.db.models.expressions import Combinable, F, OrderBy
  15from django.forms.models import (
  16    BaseModelForm,
  17    BaseModelFormSet,
  18    _get_foreign_key,
  19)
  20from django.template import engines
  21from django.template.backends.django import DjangoTemplates
  22from django.utils.module_loading import import_string
  23
  24
  25def _issubclass(cls, classinfo):
  26    """
  27    issubclass() variant that doesn't raise an exception if cls isn't a
  28    class.
  29    """
  30    try:
  31        return issubclass(cls, classinfo)
  32    except TypeError:
  33        return False
  34
  35
  36def _contains_subclass(class_path, candidate_paths):
  37    """
  38    Return whether or not a dotted class path (or a subclass of that class) is
  39    found in a list of candidate paths.
  40    """
  41    cls = import_string(class_path)
  42    for path in candidate_paths:
  43        try:
  44            candidate_cls = import_string(path)
  45        except ImportError:
  46            # ImportErrors are raised elsewhere.
  47            continue
  48        if _issubclass(candidate_cls, cls):
  49            return True
  50    return False
  51
  52
  53def check_admin_app(app_configs, **kwargs):
  54    from django.contrib.admin.sites import all_sites
  55
  56    errors = []
  57    for site in all_sites:
  58        errors.extend(site.check(app_configs))
  59    return errors
  60
  61
  62def check_dependencies(**kwargs):
  63    """
  64    Check that the admin's dependencies are correctly installed.
  65    """
  66    if not apps.is_installed("django.contrib.admin"):
  67        return []
  68    errors = []
  69    app_dependencies = (
  70        ("django.contrib.contenttypes", 401),
  71        ("django.contrib.auth", 405),
  72        ("django.contrib.messages", 406),
  73    )
  74    for app_name, error_code in app_dependencies:
  75        if not apps.is_installed(app_name):
  76            errors.append(
  77                checks.Error(
  78                    "'%s' must be in INSTALLED_APPS in order to use the admin "
  79                    "application." % app_name,
  80                    id="admin.E%d" % error_code,
  81                )
  82            )
  83    for engine in engines.all():
  84        if isinstance(engine, DjangoTemplates):
  85            django_templates_instance = engine.engine
  86            break
  87    else:
  88        django_templates_instance = None
  89    if not django_templates_instance:
  90        errors.append(
  91            checks.Error(
  92                "A 'django.template.backends.django.DjangoTemplates' instance "
  93                "must be configured in TEMPLATES in order to use the admin "
  94                "application.",
  95                id="admin.E403",
  96            )
  97        )
  98    else:
  99        if (
 100            "django.contrib.auth.context_processors.auth"
 101            not in django_templates_instance.context_processors
 102            and _contains_subclass(
 103                "django.contrib.auth.backends.ModelBackend",
 104                settings.AUTHENTICATION_BACKENDS,
 105            )
 106        ):
 107            errors.append(
 108                checks.Error(
 109                    "'django.contrib.auth.context_processors.auth' must be "
 110                    "enabled in DjangoTemplates (TEMPLATES) if using the default "
 111                    "auth backend in order to use the admin application.",
 112                    id="admin.E402",
 113                )
 114            )
 115        if (
 116            "django.contrib.messages.context_processors.messages"
 117            not in django_templates_instance.context_processors
 118        ):
 119            errors.append(
 120                checks.Error(
 121                    "'django.contrib.messages.context_processors.messages' must "
 122                    "be enabled in DjangoTemplates (TEMPLATES) in order to use "
 123                    "the admin application.",
 124                    id="admin.E404",
 125                )
 126            )
 127
 128    if not _contains_subclass(
 129        "django.contrib.auth.middleware.AuthenticationMiddleware", settings.MIDDLEWARE
 130    ):
 131        errors.append(
 132            checks.Error(
 133                "'django.contrib.auth.middleware.AuthenticationMiddleware' must "
 134                "be in MIDDLEWARE in order to use the admin application.",
 135                id="admin.E408",
 136            )
 137        )
 138    if not _contains_subclass(
 139        "django.contrib.messages.middleware.MessageMiddleware", settings.MIDDLEWARE
 140    ):
 141        errors.append(
 142            checks.Error(
 143                "'django.contrib.messages.middleware.MessageMiddleware' must "
 144                "be in MIDDLEWARE in order to use the admin application.",
 145                id="admin.E409",
 146            )
 147        )
 148    if not _contains_subclass(
 149        "django.contrib.sessions.middleware.SessionMiddleware", settings.MIDDLEWARE
 150    ):
 151        errors.append(
 152            checks.Error(
 153                "'django.contrib.sessions.middleware.SessionMiddleware' must "
 154                "be in MIDDLEWARE in order to use the admin application.",
 155                id="admin.E410",
 156            )
 157        )
 158    return errors
 159
 160
 161class BaseModelAdminChecks:
 162    def check(self, admin_obj, **kwargs):
 163        return [
 164            *self._check_autocomplete_fields(admin_obj),
 165            *self._check_raw_id_fields(admin_obj),
 166            *self._check_fields(admin_obj),
 167            *self._check_fieldsets(admin_obj),
 168            *self._check_exclude(admin_obj),
 169            *self._check_form(admin_obj),
 170            *self._check_filter_vertical(admin_obj),
 171            *self._check_filter_horizontal(admin_obj),
 172            *self._check_radio_fields(admin_obj),
 173            *self._check_prepopulated_fields(admin_obj),
 174            *self._check_view_on_site_url(admin_obj),
 175            *self._check_ordering(admin_obj),
 176            *self._check_readonly_fields(admin_obj),
 177        ]
 178
 179    def _check_autocomplete_fields(self, obj):
 180        """
 181        Check that `autocomplete_fields` is a list or tuple of model fields.
 182        """
 183        if not isinstance(obj.autocomplete_fields, (list, tuple)):
 184            return must_be(
 185                "a list or tuple",
 186                option="autocomplete_fields",
 187                obj=obj,
 188                id="admin.E036",
 189            )
 190        else:
 191            return list(
 192                chain.from_iterable(
 193                    [
 194                        self._check_autocomplete_fields_item(
 195                            obj, field_name, "autocomplete_fields[%d]" % index
 196                        )
 197                        for index, field_name in enumerate(obj.autocomplete_fields)
 198                    ]
 199                )
 200            )
 201
 202    def _check_autocomplete_fields_item(self, obj, field_name, label):
 203        """
 204        Check that an item in `autocomplete_fields` is a ForeignKey or a
 205        ManyToManyField and that the item has a related ModelAdmin with
 206        search_fields defined.
 207        """
 208        try:
 209            field = obj.model._meta.get_field(field_name)
 210        except FieldDoesNotExist:
 211            return refer_to_missing_field(
 212                field=field_name, option=label, obj=obj, id="admin.E037"
 213            )
 214        else:
 215            if not field.many_to_many and not isinstance(field, models.ForeignKey):
 216                return must_be(
 217                    "a foreign key or a many-to-many field",
 218                    option=label,
 219                    obj=obj,
 220                    id="admin.E038",
 221                )
 222            related_admin = obj.admin_site._registry.get(field.remote_field.model)
 223            if related_admin is None:
 224                return [
 225                    checks.Error(
 226                        'An admin for model "%s" has to be registered '
 227                        "to be referenced by %s.autocomplete_fields."
 228                        % (field.remote_field.model.__name__, type(obj).__name__,),
 229                        obj=obj.__class__,
 230                        id="admin.E039",
 231                    )
 232                ]
 233            elif not related_admin.search_fields:
 234                return [
 235                    checks.Error(
 236                        '%s must define "search_fields", because it\'s '
 237                        "referenced by %s.autocomplete_fields."
 238                        % (related_admin.__class__.__name__, type(obj).__name__,),
 239                        obj=obj.__class__,
 240                        id="admin.E040",
 241                    )
 242                ]
 243            return []
 244
 245    def _check_raw_id_fields(self, obj):
 246        """ Check that `raw_id_fields` only contains field names that are listed
 247        on the model. """
 248
 249        if not isinstance(obj.raw_id_fields, (list, tuple)):
 250            return must_be(
 251                "a list or tuple", option="raw_id_fields", obj=obj, id="admin.E001"
 252            )
 253        else:
 254            return list(
 255                chain.from_iterable(
 256                    self._check_raw_id_fields_item(
 257                        obj, field_name, "raw_id_fields[%d]" % index
 258                    )
 259                    for index, field_name in enumerate(obj.raw_id_fields)
 260                )
 261            )
 262
 263    def _check_raw_id_fields_item(self, obj, field_name, label):
 264        """ Check an item of `raw_id_fields`, i.e. check that field named
 265        `field_name` exists in model `model` and is a ForeignKey or a
 266        ManyToManyField. """
 267
 268        try:
 269            field = obj.model._meta.get_field(field_name)
 270        except FieldDoesNotExist:
 271            return refer_to_missing_field(
 272                field=field_name, option=label, obj=obj, id="admin.E002"
 273            )
 274        else:
 275            if not field.many_to_many and not isinstance(field, models.ForeignKey):
 276                return must_be(
 277                    "a foreign key or a many-to-many field",
 278                    option=label,
 279                    obj=obj,
 280                    id="admin.E003",
 281                )
 282            else:
 283                return []
 284
 285    def _check_fields(self, obj):
 286        """ Check that `fields` only refer to existing fields, doesn't contain
 287        duplicates. Check if at most one of `fields` and `fieldsets` is defined.
 288        """
 289
 290        if obj.fields is None:
 291            return []
 292        elif not isinstance(obj.fields, (list, tuple)):
 293            return must_be("a list or tuple", option="fields", obj=obj, id="admin.E004")
 294        elif obj.fieldsets:
 295            return [
 296                checks.Error(
 297                    "Both 'fieldsets' and 'fields' are specified.",
 298                    obj=obj.__class__,
 299                    id="admin.E005",
 300                )
 301            ]
 302        fields = flatten(obj.fields)
 303        if len(fields) != len(set(fields)):
 304            return [
 305                checks.Error(
 306                    "The value of 'fields' contains duplicate field(s).",
 307                    obj=obj.__class__,
 308                    id="admin.E006",
 309                )
 310            ]
 311
 312        return list(
 313            chain.from_iterable(
 314                self._check_field_spec(obj, field_name, "fields")
 315                for field_name in obj.fields
 316            )
 317        )
 318
 319    def _check_fieldsets(self, obj):
 320        """ Check that fieldsets is properly formatted and doesn't contain
 321        duplicates. """
 322
 323        if obj.fieldsets is None:
 324            return []
 325        elif not isinstance(obj.fieldsets, (list, tuple)):
 326            return must_be(
 327                "a list or tuple", option="fieldsets", obj=obj, id="admin.E007"
 328            )
 329        else:
 330            seen_fields = []
 331            return list(
 332                chain.from_iterable(
 333                    self._check_fieldsets_item(
 334                        obj, fieldset, "fieldsets[%d]" % index, seen_fields
 335                    )
 336                    for index, fieldset in enumerate(obj.fieldsets)
 337                )
 338            )
 339
 340    def _check_fieldsets_item(self, obj, fieldset, label, seen_fields):
 341        """ Check an item of `fieldsets`, i.e. check that this is a pair of a
 342        set name and a dictionary containing "fields" key. """
 343
 344        if not isinstance(fieldset, (list, tuple)):
 345            return must_be("a list or tuple", option=label, obj=obj, id="admin.E008")
 346        elif len(fieldset) != 2:
 347            return must_be("of length 2", option=label, obj=obj, id="admin.E009")
 348        elif not isinstance(fieldset[1], dict):
 349            return must_be(
 350                "a dictionary", option="%s[1]" % label, obj=obj, id="admin.E010"
 351            )
 352        elif "fields" not in fieldset[1]:
 353            return [
 354                checks.Error(
 355                    "The value of '%s[1]' must contain the key 'fields'." % label,
 356                    obj=obj.__class__,
 357                    id="admin.E011",
 358                )
 359            ]
 360        elif not isinstance(fieldset[1]["fields"], (list, tuple)):
 361            return must_be(
 362                "a list or tuple",
 363                option="%s[1]['fields']" % label,
 364                obj=obj,
 365                id="admin.E008",
 366            )
 367
 368        seen_fields.extend(flatten(fieldset[1]["fields"]))
 369        if len(seen_fields) != len(set(seen_fields)):
 370            return [
 371                checks.Error(
 372                    "There are duplicate field(s) in '%s[1]'." % label,
 373                    obj=obj.__class__,
 374                    id="admin.E012",
 375                )
 376            ]
 377        return list(
 378            chain.from_iterable(
 379                self._check_field_spec(obj, fieldset_fields, '%s[1]["fields"]' % label)
 380                for fieldset_fields in fieldset[1]["fields"]
 381            )
 382        )
 383
 384    def _check_field_spec(self, obj, fields, label):
 385        """ `fields` should be an item of `fields` or an item of
 386        fieldset[1]['fields'] for any `fieldset` in `fieldsets`. It should be a
 387        field name or a tuple of field names. """
 388
 389        if isinstance(fields, tuple):
 390            return list(
 391                chain.from_iterable(
 392                    self._check_field_spec_item(
 393                        obj, field_name, "%s[%d]" % (label, index)
 394                    )
 395                    for index, field_name in enumerate(fields)
 396                )
 397            )
 398        else:
 399            return self._check_field_spec_item(obj, fields, label)
 400
 401    def _check_field_spec_item(self, obj, field_name, label):
 402        if field_name in obj.readonly_fields:
 403            # Stuff can be put in fields that isn't actually a model field if
 404            # it's in readonly_fields, readonly_fields will handle the
 405            # validation of such things.
 406            return []
 407        else:
 408            try:
 409                field = obj.model._meta.get_field(field_name)
 410            except FieldDoesNotExist:
 411                # If we can't find a field on the model that matches, it could
 412                # be an extra field on the form.
 413                return []
 414            else:
 415                if (
 416                    isinstance(field, models.ManyToManyField)
 417                    and not field.remote_field.through._meta.auto_created
 418                ):
 419                    return [
 420                        checks.Error(
 421                            "The value of '%s' cannot include the ManyToManyField '%s', "
 422                            "because that field manually specifies a relationship model."
 423                            % (label, field_name),
 424                            obj=obj.__class__,
 425                            id="admin.E013",
 426                        )
 427                    ]
 428                else:
 429                    return []
 430
 431    def _check_exclude(self, obj):
 432        """ Check that exclude is a sequence without duplicates. """
 433
 434        if obj.exclude is None:  # default value is None
 435            return []
 436        elif not isinstance(obj.exclude, (list, tuple)):
 437            return must_be(
 438                "a list or tuple", option="exclude", obj=obj, id="admin.E014"
 439            )
 440        elif len(obj.exclude) > len(set(obj.exclude)):
 441            return [
 442                checks.Error(
 443                    "The value of 'exclude' contains duplicate field(s).",
 444                    obj=obj.__class__,
 445                    id="admin.E015",
 446                )
 447            ]
 448        else:
 449            return []
 450
 451    def _check_form(self, obj):
 452        """ Check that form subclasses BaseModelForm. """
 453        if not _issubclass(obj.form, BaseModelForm):
 454            return must_inherit_from(
 455                parent="BaseModelForm", option="form", obj=obj, id="admin.E016"
 456            )
 457        else:
 458            return []
 459
 460    def _check_filter_vertical(self, obj):
 461        """ Check that filter_vertical is a sequence of field names. """
 462        if not isinstance(obj.filter_vertical, (list, tuple)):
 463            return must_be(
 464                "a list or tuple", option="filter_vertical", obj=obj, id="admin.E017"
 465            )
 466        else:
 467            return list(
 468                chain.from_iterable(
 469                    self._check_filter_item(
 470                        obj, field_name, "filter_vertical[%d]" % index
 471                    )
 472                    for index, field_name in enumerate(obj.filter_vertical)
 473                )
 474            )
 475
 476    def _check_filter_horizontal(self, obj):
 477        """ Check that filter_horizontal is a sequence of field names. """
 478        if not isinstance(obj.filter_horizontal, (list, tuple)):
 479            return must_be(
 480                "a list or tuple", option="filter_horizontal", obj=obj, id="admin.E018"
 481            )
 482        else:
 483            return list(
 484                chain.from_iterable(
 485                    self._check_filter_item(
 486                        obj, field_name, "filter_horizontal[%d]" % index
 487                    )
 488                    for index, field_name in enumerate(obj.filter_horizontal)
 489                )
 490            )
 491
 492    def _check_filter_item(self, obj, field_name, label):
 493        """ Check one item of `filter_vertical` or `filter_horizontal`, i.e.
 494        check that given field exists and is a ManyToManyField. """
 495
 496        try:
 497            field = obj.model._meta.get_field(field_name)
 498        except FieldDoesNotExist:
 499            return refer_to_missing_field(
 500                field=field_name, option=label, obj=obj, id="admin.E019"
 501            )
 502        else:
 503            if not field.many_to_many:
 504                return must_be(
 505                    "a many-to-many field", option=label, obj=obj, id="admin.E020"
 506                )
 507            else:
 508                return []
 509
 510    def _check_radio_fields(self, obj):
 511        """ Check that `radio_fields` is a dictionary. """
 512        if not isinstance(obj.radio_fields, dict):
 513            return must_be(
 514                "a dictionary", option="radio_fields", obj=obj, id="admin.E021"
 515            )
 516        else:
 517            return list(
 518                chain.from_iterable(
 519                    self._check_radio_fields_key(obj, field_name, "radio_fields")
 520                    + self._check_radio_fields_value(
 521                        obj, val, 'radio_fields["%s"]' % field_name
 522                    )
 523                    for field_name, val in obj.radio_fields.items()
 524                )
 525            )
 526
 527    def _check_radio_fields_key(self, obj, field_name, label):
 528        """ Check that a key of `radio_fields` dictionary is name of existing
 529        field and that the field is a ForeignKey or has `choices` defined. """
 530
 531        try:
 532            field = obj.model._meta.get_field(field_name)
 533        except FieldDoesNotExist:
 534            return refer_to_missing_field(
 535                field=field_name, option=label, obj=obj, id="admin.E022"
 536            )
 537        else:
 538            if not (isinstance(field, models.ForeignKey) or field.choices):
 539                return [
 540                    checks.Error(
 541                        "The value of '%s' refers to '%s', which is not an "
 542                        "instance of ForeignKey, and does not have a 'choices' definition."
 543                        % (label, field_name),
 544                        obj=obj.__class__,
 545                        id="admin.E023",
 546                    )
 547                ]
 548            else:
 549                return []
 550
 551    def _check_radio_fields_value(self, obj, val, label):
 552        """ Check type of a value of `radio_fields` dictionary. """
 553
 554        from django.contrib.admin.options import HORIZONTAL, VERTICAL
 555
 556        if val not in (HORIZONTAL, VERTICAL):
 557            return [
 558                checks.Error(
 559                    "The value of '%s' must be either admin.HORIZONTAL or admin.VERTICAL."
 560                    % label,
 561                    obj=obj.__class__,
 562                    id="admin.E024",
 563                )
 564            ]
 565        else:
 566            return []
 567
 568    def _check_view_on_site_url(self, obj):
 569        if not callable(obj.view_on_site) and not isinstance(obj.view_on_site, bool):
 570            return [
 571                checks.Error(
 572                    "The value of 'view_on_site' must be a callable or a boolean value.",
 573                    obj=obj.__class__,
 574                    id="admin.E025",
 575                )
 576            ]
 577        else:
 578            return []
 579
 580    def _check_prepopulated_fields(self, obj):
 581        """ Check that `prepopulated_fields` is a dictionary containing allowed
 582        field types. """
 583        if not isinstance(obj.prepopulated_fields, dict):
 584            return must_be(
 585                "a dictionary", option="prepopulated_fields", obj=obj, id="admin.E026"
 586            )
 587        else:
 588            return list(
 589                chain.from_iterable(
 590                    self._check_prepopulated_fields_key(
 591                        obj, field_name, "prepopulated_fields"
 592                    )
 593                    + self._check_prepopulated_fields_value(
 594                        obj, val, 'prepopulated_fields["%s"]' % field_name
 595                    )
 596                    for field_name, val in obj.prepopulated_fields.items()
 597                )
 598            )
 599
 600    def _check_prepopulated_fields_key(self, obj, field_name, label):
 601        """ Check a key of `prepopulated_fields` dictionary, i.e. check that it
 602        is a name of existing field and the field is one of the allowed types.
 603        """
 604
 605        try:
 606            field = obj.model._meta.get_field(field_name)
 607        except FieldDoesNotExist:
 608            return refer_to_missing_field(
 609                field=field_name, option=label, obj=obj, id="admin.E027"
 610            )
 611        else:
 612            if isinstance(
 613                field, (models.DateTimeField, models.ForeignKey, models.ManyToManyField)
 614            ):
 615                return [
 616                    checks.Error(
 617                        "The value of '%s' refers to '%s', which must not be a DateTimeField, "
 618                        "a ForeignKey, a OneToOneField, or a ManyToManyField."
 619                        % (label, field_name),
 620                        obj=obj.__class__,
 621                        id="admin.E028",
 622                    )
 623                ]
 624            else:
 625                return []
 626
 627    def _check_prepopulated_fields_value(self, obj, val, label):
 628        """ Check a value of `prepopulated_fields` dictionary, i.e. it's an
 629        iterable of existing fields. """
 630
 631        if not isinstance(val, (list, tuple)):
 632            return must_be("a list or tuple", option=label, obj=obj, id="admin.E029")
 633        else:
 634            return list(
 635                chain.from_iterable(
 636                    self._check_prepopulated_fields_value_item(
 637                        obj, subfield_name, "%s[%r]" % (label, index)
 638                    )
 639                    for index, subfield_name in enumerate(val)
 640                )
 641            )
 642
 643    def _check_prepopulated_fields_value_item(self, obj, field_name, label):
 644        """ For `prepopulated_fields` equal to {"slug": ("title",)},
 645        `field_name` is "title". """
 646
 647        try:
 648            obj.model._meta.get_field(field_name)
 649        except FieldDoesNotExist:
 650            return refer_to_missing_field(
 651                field=field_name, option=label, obj=obj, id="admin.E030"
 652            )
 653        else:
 654            return []
 655
 656    def _check_ordering(self, obj):
 657        """ Check that ordering refers to existing fields or is random. """
 658
 659        # ordering = None
 660        if obj.ordering is None:  # The default value is None
 661            return []
 662        elif not isinstance(obj.ordering, (list, tuple)):
 663            return must_be(
 664                "a list or tuple", option="ordering", obj=obj, id="admin.E031"
 665            )
 666        else:
 667            return list(
 668                chain.from_iterable(
 669                    self._check_ordering_item(obj, field_name, "ordering[%d]" % index)
 670                    for index, field_name in enumerate(obj.ordering)
 671                )
 672            )
 673
 674    def _check_ordering_item(self, obj, field_name, label):
 675        """ Check that `ordering` refers to existing fields. """
 676        if isinstance(field_name, (Combinable, OrderBy)):
 677            if not isinstance(field_name, OrderBy):
 678                field_name = field_name.asc()
 679            if isinstance(field_name.expression, F):
 680                field_name = field_name.expression.name
 681            else:
 682                return []
 683        if field_name == "?" and len(obj.ordering) != 1:
 684            return [
 685                checks.Error(
 686                    "The value of 'ordering' has the random ordering marker '?', "
 687                    "but contains other fields as well.",
 688                    hint='Either remove the "?", or remove the other fields.',
 689                    obj=obj.__class__,
 690                    id="admin.E032",
 691                )
 692            ]
 693        elif field_name == "?":
 694            return []
 695        elif LOOKUP_SEP in field_name:
 696            # Skip ordering in the format field1__field2 (FIXME: checking
 697            # this format would be nice, but it's a little fiddly).
 698            return []
 699        else:
 700            if field_name.startswith("-"):
 701                field_name = field_name[1:]
 702            if field_name == "pk":
 703                return []
 704            try:
 705                obj.model._meta.get_field(field_name)
 706            except FieldDoesNotExist:
 707                return refer_to_missing_field(
 708                    field=field_name, option=label, obj=obj, id="admin.E033"
 709                )
 710            else:
 711                return []
 712
 713    def _check_readonly_fields(self, obj):
 714        """ Check that readonly_fields refers to proper attribute or field. """
 715
 716        if obj.readonly_fields == ():
 717            return []
 718        elif not isinstance(obj.readonly_fields, (list, tuple)):
 719            return must_be(
 720                "a list or tuple", option="readonly_fields", obj=obj, id="admin.E034"
 721            )
 722        else:
 723            return list(
 724                chain.from_iterable(
 725                    self._check_readonly_fields_item(
 726                        obj, field_name, "readonly_fields[%d]" % index
 727                    )
 728                    for index, field_name in enumerate(obj.readonly_fields)
 729                )
 730            )
 731
 732    def _check_readonly_fields_item(self, obj, field_name, label):
 733        if callable(field_name):
 734            return []
 735        elif hasattr(obj, field_name):
 736            return []
 737        elif hasattr(obj.model, field_name):
 738            return []
 739        else:
 740            try:
 741                obj.model._meta.get_field(field_name)
 742            except FieldDoesNotExist:
 743                return [
 744                    checks.Error(
 745                        "The value of '%s' is not a callable, an attribute of '%s', or an attribute of '%s.%s'."
 746                        % (
 747                            label,
 748                            obj.__class__.__name__,
 749                            obj.model._meta.app_label,
 750                            obj.model._meta.object_name,
 751                        ),
 752                        obj=obj.__class__,
 753                        id="admin.E035",
 754                    )
 755                ]
 756            else:
 757                return []
 758
 759
 760class ModelAdminChecks(BaseModelAdminChecks):
 761    def check(self, admin_obj, **kwargs):
 762        return [
 763            *super().check(admin_obj),
 764            *self._check_save_as(admin_obj),
 765            *self._check_save_on_top(admin_obj),
 766            *self._check_inlines(admin_obj),
 767            *self._check_list_display(admin_obj),
 768            *self._check_list_display_links(admin_obj),
 769            *self._check_list_filter(admin_obj),
 770            *self._check_list_select_related(admin_obj),
 771            *self._check_list_per_page(admin_obj),
 772            *self._check_list_max_show_all(admin_obj),
 773            *self._check_list_editable(admin_obj),
 774            *self._check_search_fields(admin_obj),
 775            *self._check_date_hierarchy(admin_obj),
 776            *self._check_action_permission_methods(admin_obj),
 777            *self._check_actions_uniqueness(admin_obj),
 778        ]
 779
 780    def _check_save_as(self, obj):
 781        """ Check save_as is a boolean. """
 782
 783        if not isinstance(obj.save_as, bool):
 784            return must_be("a boolean", option="save_as", obj=obj, id="admin.E101")
 785        else:
 786            return []
 787
 788    def _check_save_on_top(self, obj):
 789        """ Check save_on_top is a boolean. """
 790
 791        if not isinstance(obj.save_on_top, bool):
 792            return must_be("a boolean", option="save_on_top", obj=obj, id="admin.E102")
 793        else:
 794            return []
 795
 796    def _check_inlines(self, obj):
 797        """ Check all inline model admin classes. """
 798
 799        if not isinstance(obj.inlines, (list, tuple)):
 800            return must_be(
 801                "a list or tuple", option="inlines", obj=obj, id="admin.E103"
 802            )
 803        else:
 804            return list(
 805                chain.from_iterable(
 806                    self._check_inlines_item(obj, item, "inlines[%d]" % index)
 807                    for index, item in enumerate(obj.inlines)
 808                )
 809            )
 810
 811    def _check_inlines_item(self, obj, inline, label):
 812        """ Check one inline model admin. """
 813        try:
 814            inline_label = inline.__module__ + "." + inline.__name__
 815        except AttributeError:
 816            return [
 817                checks.Error(
 818                    "'%s' must inherit from 'InlineModelAdmin'." % obj,
 819                    obj=obj.__class__,
 820                    id="admin.E104",
 821                )
 822            ]
 823
 824        from django.contrib.admin.options import InlineModelAdmin
 825
 826        if not _issubclass(inline, InlineModelAdmin):
 827            return [
 828                checks.Error(
 829                    "'%s' must inherit from 'InlineModelAdmin'." % inline_label,
 830                    obj=obj.__class__,
 831                    id="admin.E104",
 832                )
 833            ]
 834        elif not inline.model:
 835            return [
 836                checks.Error(
 837                    "'%s' must have a 'model' attribute." % inline_label,
 838                    obj=obj.__class__,
 839                    id="admin.E105",
 840                )
 841            ]
 842        elif not _issubclass(inline.model, models.Model):
 843            return must_be(
 844                "a Model", option="%s.model" % inline_label, obj=obj, id="admin.E106"
 845            )
 846        else:
 847            return inline(obj.model, obj.admin_site).check()
 848
 849    def _check_list_display(self, obj):
 850        """ Check that list_display only contains fields or usable attributes.
 851        """
 852
 853        if not isinstance(obj.list_display, (list, tuple)):
 854            return must_be(
 855                "a list or tuple", option="list_display", obj=obj, id="admin.E107"
 856            )
 857        else:
 858            return list(
 859                chain.from_iterable(
 860                    self._check_list_display_item(obj, item, "list_display[%d]" % index)
 861                    for index, item in enumerate(obj.list_display)
 862                )
 863            )
 864
 865    def _check_list_display_item(self, obj, item, label):
 866        if callable(item):
 867            return []
 868        elif hasattr(obj, item):
 869            return []
 870        try:
 871            field = obj.model._meta.get_field(item)
 872        except FieldDoesNotExist:
 873            try:
 874                field = getattr(obj.model, item)
 875            except AttributeError:
 876                return [
 877                    checks.Error(
 878                        "The value of '%s' refers to '%s', which is not a "
 879                        "callable, an attribute of '%s', or an attribute or "
 880                        "method on '%s.%s'."
 881                        % (
 882                            label,
 883                            item,
 884                            obj.__class__.__name__,
 885                            obj.model._meta.app_label,
 886                            obj.model._meta.object_name,
 887                        ),
 888                        obj=obj.__class__,
 889                        id="admin.E108",
 890                    )
 891                ]
 892        if isinstance(field, models.ManyToManyField):
 893            return [
 894                checks.Error(
 895                    "The value of '%s' must not be a ManyToManyField." % label,
 896                    obj=obj.__class__,
 897                    id="admin.E109",
 898                )
 899            ]
 900        return []
 901
 902    def _check_list_display_links(self, obj):
 903        """ Check that list_display_links is a unique subset of list_display.
 904        """
 905        from django.contrib.admin.options import ModelAdmin
 906
 907        if obj.list_display_links is None:
 908            return []
 909        elif not isinstance(obj.list_display_links, (list, tuple)):
 910            return must_be(
 911                "a list, a tuple, or None",
 912                option="list_display_links",
 913                obj=obj,
 914                id="admin.E110",
 915            )
 916        # Check only if ModelAdmin.get_list_display() isn't overridden.
 917        elif obj.get_list_display.__func__ is ModelAdmin.get_list_display:
 918            return list(
 919                chain.from_iterable(
 920                    self._check_list_display_links_item(
 921                        obj, field_name, "list_display_links[%d]" % index
 922                    )
 923                    for index, field_name in enumerate(obj.list_display_links)
 924                )
 925            )
 926        return []
 927
 928    def _check_list_display_links_item(self, obj, field_name, label):
 929        if field_name not in obj.list_display:
 930            return [
 931                checks.Error(
 932                    "The value of '%s' refers to '%s', which is not defined in 'list_display'."
 933                    % (label, field_name),
 934                    obj=obj.__class__,
 935                    id="admin.E111",
 936                )
 937            ]
 938        else:
 939            return []
 940
 941    def _check_list_filter(self, obj):
 942        if not isinstance(obj.list_filter, (list, tuple)):
 943            return must_be(
 944                "a list or tuple", option="list_filter", obj=obj, id="admin.E112"
 945            )
 946        else:
 947            return list(
 948                chain.from_iterable(
 949                    self._check_list_filter_item(obj, item, "list_filter[%d]" % index)
 950                    for index, item in enumerate(obj.list_filter)
 951                )
 952            )
 953
 954    def _check_list_filter_item(self, obj, item, label):
 955        """
 956        Check one item of `list_filter`, i.e. check if it is one of three options:
 957        1. 'field' -- a basic field filter, possibly w/ relationships (e.g.
 958           'field__rel')
 959        2. ('field', SomeFieldListFilter) - a field-based list filter class
 960        3. SomeListFilter - a non-field list filter class
 961        """
 962
 963        from django.contrib.admin import ListFilter, FieldListFilter
 964
 965        if callable(item) and not isinstance(item, models.Field):
 966            # If item is option 3, it should be a ListFilter...
 967            if not _issubclass(item, ListFilter):
 968                return must_inherit_from(
 969                    parent="ListFilter", option=label, obj=obj, id="admin.E113"
 970                )
 971            # ...  but not a FieldListFilter.
 972            elif issubclass(item, FieldListFilter):
 973                return [
 974                    checks.Error(
 975                        "The value of '%s' must not inherit from 'FieldListFilter'."
 976                        % label,
 977                        obj=obj.__class__,
 978                        id="admin.E114",
 979                    )
 980                ]
 981            else:
 982                return []
 983        elif isinstance(item, (tuple, list)):
 984            # item is option #2
 985            field, list_filter_class = item
 986            if not _issubclass(list_filter_class, FieldListFilter):
 987                return must_inherit_from(
 988                    parent="FieldListFilter",
 989                    option="%s[1]" % label,
 990                    obj=obj,
 991                    id="admin.E115",
 992                )
 993            else:
 994                return []
 995        else:
 996            # item is option #1
 997            field = item
 998
 999            # Validate the field string
1000            try:
1001                get_fields_from_path(obj.model, field)
1002            except (NotRelationField, FieldDoesNotExist):
1003                return [
1004                    checks.Error(
1005                        "The value of '%s' refers to '%s', which does not refer to a Field."
1006                        % (label, field),
1007                        obj=obj.__class__,
1008                        id="admin.E116",
1009                    )
1010                ]
1011            else:
1012                return []
1013
1014    def _check_list_select_related(self, obj):
1015        """ Check that list_select_related is a boolean, a list or a tuple. """
1016
1017        if not isinstance(obj.list_select_related, (bool, list, tuple)):
1018            return must_be(
1019                "a boolean, tuple or list",
1020                option="list_select_related",
1021                obj=obj,
1022                id="admin.E117",
1023            )
1024        else:
1025            return []
1026
1027    def _check_list_per_page(self, obj):
1028        """ Check that list_per_page is an integer. """
1029
1030        if not isinstance(obj.list_per_page, int):
1031            return must_be(
1032                "an integer", option="list_per_page", obj=obj, id="admin.E118"
1033            )
1034        else:
1035            return []
1036
1037    def _check_list_max_show_all(self, obj):
1038        """ Check that list_max_show_all is an integer. """
1039
1040        if not isinstance(obj.list_max_show_all, int):
1041            return must_be(
1042                "an integer", option="list_max_show_all", obj=obj, id="admin.E119"
1043            )
1044        else:
1045            return []
1046
1047    def _check_list_editable(self, obj):
1048        """ Check that list_editable is a sequence of editable fields from
1049        list_display without first element. """
1050
1051        if not isinstance(obj.list_editable, (list, tuple)):
1052            return must_be(
1053                "a list or tuple", option="list_editable", obj=obj, id="admin.E120"
1054            )
1055        else:
1056            return list(
1057                chain.from_iterable(
1058                    self._check_list_editable_item(
1059                        obj, item, "list_editable[%d]" % index
1060                    )
1061                    for index, item in enumerate(obj.list_editable)
1062                )
1063            )
1064
1065    def _check_list_editable_item(self, obj, field_name, label):
1066        try:
1067            field = obj.model._meta.get_field(field_name)
1068        except FieldDoesNotExist:
1069            return refer_to_missing_field(
1070                field=field_name, option=label, obj=obj, id="admin.E121"
1071            )
1072        else:
1073            if field_name not in obj.list_display:
1074                return [
1075                    checks.Error(
1076                        "The value of '%s' refers to '%s', which is not "
1077                        "contained in 'list_display'." % (label, field_name),
1078                        obj=obj.__class__,
1079                        id="admin.E122",
1080                    )
1081                ]
1082            elif obj.list_display_links and field_name in obj.list_display_links:
1083                return [
1084                    checks.Error(
1085                        "The value of '%s' cannot be in both 'list_editable' and 'list_display_links'."
1086                        % field_name,
1087                        obj=obj.__class__,
1088                        id="admin.E123",
1089                    )
1090                ]
1091            # If list_display[0] is in list_editable, check that
1092            # list_display_links is set. See #22792 and #26229 for use cases.
1093            elif (
1094                obj.list_display[0] == field_name
1095                and not obj.list_display_links
1096                and obj.list_display_links is not None
1097            ):
1098                return [
1099                    checks.Error(
1100                        "The value of '%s' refers to the first field in 'list_display' ('%s'), "
1101                        "which cannot be used unless 'list_display_links' is set."
1102                        % (label, obj.list_display[0]),
1103                        obj=obj.__class__,
1104                        id="admin.E124",
1105                    )
1106                ]
1107            elif not field.editable:
1108                return [
1109                    checks.Error(
1110                        "The value of '%s' refers to '%s', which is not editable through the admin."
1111                        % (label, field_name),
1112                        obj=obj.__class__,
1113                        id="admin.E125",
1114                    )
1115                ]
1116            else:
1117                return []
1118
1119    def _check_search_fields(self, obj):
1120        """ Check search_fields is a sequence. """
1121
1122        if not isinstance(obj.search_fields, (list, tuple)):
1123            return must_be(
1124                "a list or tuple", option="search_fields", obj=obj, id="admin.E126"
1125            )
1126        else:
1127            return []
1128
1129    def _check_date_hierarchy(self, obj):
1130        """ Check that date_hierarchy refers to DateField or DateTimeField. """
1131
1132        if obj.date_hierarchy is None:
1133            return []
1134        else:
1135            try:
1136                field = get_fields_from_path(obj.model, obj.date_hierarchy)[-1]
1137            except (NotRelationField, FieldDoesNotExist):
1138                return [
1139                    checks.Error(
1140                        "The value of 'date_hierarchy' refers to '%s', which "
1141                        "does not refer to a Field." % obj.date_hierarchy,
1142                        obj=obj.__class__,
1143                        id="admin.E127",
1144                    )
1145                ]
1146            else:
1147                if not isinstance(field, (models.DateField, models.DateTimeField)):
1148                    return must_be(
1149                        "a DateField or DateTimeField",
1150                        option="date_hierarchy",
1151                        obj=obj,
1152                        id="admin.E128",
1153                    )
1154                else:
1155                    return []
1156
1157    def _check_action_permission_methods(self, obj):
1158        """
1159        Actions with an allowed_permission attribute require the ModelAdmin to
1160        implement a has_<perm>_permission() method for each permission.
1161        """
1162        actions = obj._get_base_actions()
1163        errors = []
1164        for func, name, _ in actions:
1165            if not hasattr(func, "allowed_permissions"):
1166                continue
1167            for permission in func.allowed_permissions:
1168                method_name = "has_%s_permission" % permission
1169                if not hasattr(obj, method_name):
1170                    errors.append(
1171                        checks.Error(
1172                            "%s must define a %s() method for the %s action."
1173                            % (obj.__class__.__name__, method_name, func.__name__,),
1174                            obj=obj.__class__,
1175                            id="admin.E129",
1176                        )
1177                    )
1178        return errors
1179
1180    def _check_actions_uniqueness(self, obj):
1181        """Check that every action has a unique __name__."""
1182        names = [name for _, name, _ in obj._get_base_actions()]
1183        if len(names) != len(set(names)):
1184            return [
1185                checks.Error(
1186                    "__name__ attributes of actions defined in %s must be "
1187                    "unique." % obj.__class__,
1188                    obj=obj.__class__,
1189                    id="admin.E130",
1190                )
1191            ]
1192        return []
1193
1194
1195class InlineModelAdminChecks(BaseModelAdminChecks):
1196    def check(self, inline_obj, **kwargs):
1197        parent_model = inline_obj.parent_model
1198        return [
1199            *super().check(inline_obj),
1200            *self._check_relation(inline_obj, parent_model),
1201            *self._check_exclude_of_parent_model(inline_obj, parent_model),
1202            *self._check_extra(inline_obj),
1203            *self._check_max_num(inline_obj),
1204            *self._check_min_num(inline_obj),
1205            *self._check_formset(inline_obj),
1206        ]
1207
1208    def _check_exclude_of_parent_model(self, obj, parent_model):
1209        # Do not perform more specific checks if the base checks result in an
1210        # error.
1211        errors = super()._check_exclude(obj)
1212        if errors:
1213            return []
1214
1215        # Skip if `fk_name` is invalid.
1216        if self._check_relation(obj, parent_model):
1217            return []
1218
1219        if obj.exclude is None:
1220            return []
1221
1222        fk = _get_foreign_key(parent_model, obj.model, fk_name=obj.fk_name)
1223        if fk.name in obj.exclude:
1224            return [
1225                checks.Error(
1226                    "Cannot exclude the field '%s', because it is the foreign key "
1227                    "to the parent model '%s.%s'."
1228                    % (
1229                        fk.name,
1230                        parent_model._meta.app_label,
1231                        parent_model._meta.object_name,
1232                    ),
1233                    obj=obj.__class__,
1234                    id="admin.E201",
1235                )
1236            ]
1237        else:
1238            return []
1239
1240    def _check_relation(self, obj, parent_model):
1241        try:
1242            _get_foreign_key(parent_model, obj.model, fk_name=obj.fk_name)
1243        except ValueError as e:
1244            return [checks.Error(e.args[0], obj=obj.__class__, id="admin.E202")]
1245        else:
1246            return []
1247
1248    def _check_extra(self, obj):
1249        """ Check that extra is an integer. """
1250
1251        if not isinstance(obj.extra, int):
1252            return must_be("an integer", option="extra", obj=obj, id="admin.E203")
1253        else:
1254            return []
1255
1256    def _check_max_num(self, obj):
1257        """ Check that max_num is an integer. """
1258
1259        if obj.max_num is None:
1260            return []
1261        elif not isinstance(obj.max_num, int):
1262            return must_be("an integer", option="max_num", obj=obj, id="admin.E204")
1263        else:
1264            return []
1265
1266    def _check_min_num(self, obj):
1267        """ Check that min_num is an integer. """
1268
1269        if obj.min_num is None:
1270            return []
1271        elif not isinstance(obj.min_num, int):
1272            return must_be("an integer", option="min_num", obj=obj, id="admin.E205")
1273        else:
1274            return []
1275
1276    def _check_formset(self, obj):
1277        """ Check formset is a subclass of BaseModelFormSet. """
1278
1279        if not _issubclass(obj.formset, BaseModelFormSet):
1280            return must_inherit_from(
1281                parent="BaseModelFormSet", option="formset", obj=obj, id="admin.E206"
1282            )
1283        else:
1284            return []
1285
1286
1287def must_be(type, option, obj, id):
1288    return [
1289        checks.Error(
1290            "The value of '%s' must be %s." % (option, type), obj=obj.__class__, id=id,
1291        ),
1292    ]
1293
1294
1295def must_inherit_from(parent, option, obj, id):
1296    return [
1297        checks.Error(
1298            "The value of '%s' must inherit from '%s'." % (option, parent),
1299            obj=obj.__class__,
1300            id=id,
1301        ),
1302    ]
1303
1304
1305def refer_to_missing_field(field, option, obj, id):
1306    return [
1307        checks.Error(
1308            "The value of '%s' refers to '%s', which is not an attribute of '%s.%s'."
1309            % (option, field, obj.model._meta.app_label, obj.model._meta.object_name),
1310            obj=obj.__class__,
1311            id=id,
1312        ),
1313    ]

django/contrib/auth/hashers.py

  1import base64
  2import binascii
  3import functools
  4import hashlib
  5import importlib
  6import warnings
  7
  8from django.conf import settings
  9from django.core.exceptions import ImproperlyConfigured
 10from django.core.signals import setting_changed
 11from django.dispatch import receiver
 12from django.utils.crypto import (
 13    constant_time_compare,
 14    get_random_string,
 15    pbkdf2,
 16)
 17from django.utils.module_loading import import_string
 18from django.utils.translation import gettext_noop as _
 19
 20UNUSABLE_PASSWORD_PREFIX = "!"  # This will never be a valid encoded hash
 21UNUSABLE_PASSWORD_SUFFIX_LENGTH = (
 22    40  # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
 23)
 24
 25
 26def is_password_usable(encoded):
 27    """
 28    Return True if this password wasn't generated by
 29    User.set_unusable_password(), i.e. make_password(None).
 30    """
 31    return encoded is None or not encoded.startswith(UNUSABLE_PASSWORD_PREFIX)
 32
 33
 34def check_password(password, encoded, setter=None, preferred="default"):
 35    """
 36    Return a boolean of whether the raw password matches the three
 37    part encoded digest.
 38
 39    If setter is specified, it'll be called when you need to
 40    regenerate the password.
 41    """
 42    if password is None or not is_password_usable(encoded):
 43        return False
 44
 45    preferred = get_hasher(preferred)
 46    try:
 47        hasher = identify_hasher(encoded)
 48    except ValueError:
 49        # encoded is gibberish or uses a hasher that's no longer installed.
 50        return False
 51
 52    hasher_changed = hasher.algorithm != preferred.algorithm
 53    must_update = hasher_changed or preferred.must_update(encoded)
 54    is_correct = hasher.verify(password, encoded)
 55
 56    # If the hasher didn't change (we don't protect against enumeration if it
 57    # does) and the password should get updated, try to close the timing gap
 58    # between the work factor of the current encoded password and the default
 59    # work factor.
 60    if not is_correct and not hasher_changed and must_update:
 61        hasher.harden_runtime(password, encoded)
 62
 63    if setter and is_correct and must_update:
 64        setter(password)
 65    return is_correct
 66
 67
 68def make_password(password, salt=None, hasher="default"):
 69    """
 70    Turn a plain-text password into a hash for database storage
 71
 72    Same as encode() but generate a new random salt. If password is None then
 73    return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
 74    which disallows logins. Additional random string reduces chances of gaining
 75    access to staff or superuser accounts. See ticket #20079 for more info.
 76    """
 77    if password is None:
 78        return UNUSABLE_PASSWORD_PREFIX + get_random_string(
 79            UNUSABLE_PASSWORD_SUFFIX_LENGTH
 80        )
 81    hasher = get_hasher(hasher)
 82    salt = salt or hasher.salt()
 83    return hasher.encode(password, salt)
 84
 85
 86@functools.lru_cache()
 87def get_hashers():
 88    hashers = []
 89    for hasher_path in settings.PASSWORD_HASHERS:
 90        hasher_cls = import_string(hasher_path)
 91        hasher = hasher_cls()
 92        if not getattr(hasher, "algorithm"):
 93            raise ImproperlyConfigured(
 94                "hasher doesn't specify an " "algorithm name: %s" % hasher_path
 95            )
 96        hashers.append(hasher)
 97    return hashers
 98
 99
100@functools.lru_cache()
101def get_hashers_by_algorithm():
102    return {hasher.algorithm: hasher for hasher in get_hashers()}
103
104
105@receiver(setting_changed)
106def reset_hashers(**kwargs):
107    if kwargs["setting"] == "PASSWORD_HASHERS":
108        get_hashers.cache_clear()
109        get_hashers_by_algorithm.cache_clear()
110
111
112def get_hasher(algorithm="default"):
113    """
114    Return an instance of a loaded password hasher.
115
116    If algorithm is 'default', return the default hasher. Lazily import hashers
117    specified in the project's settings file if needed.
118    """
119    if hasattr(algorithm, "algorithm"):
120        return algorithm
121
122    elif algorithm == "default":
123        return get_hashers()[0]
124
125    else:
126        hashers = get_hashers_by_algorithm()
127        try:
128            return hashers[algorithm]
129        except KeyError:
130            raise ValueError(
131                "Unknown password hashing algorithm '%s'. "
132                "Did you specify it in the PASSWORD_HASHERS "
133                "setting?" % algorithm
134            )
135
136
137def identify_hasher(encoded):
138    """
139    Return an instance of a loaded password hasher.
140
141    Identify hasher algorithm by examining encoded hash, and call
142    get_hasher() to return hasher. Raise ValueError if
143    algorithm cannot be identified, or if hasher is not loaded.
144    """
145    # Ancient versions of Django created plain MD5 passwords and accepted
146    # MD5 passwords with an empty salt.
147    if (len(encoded) == 32 and "$" not in encoded) or (
148        len(encoded) == 37 and encoded.startswith("md5$$")
149    ):
150        algorithm = "unsalted_md5"
151    # Ancient versions of Django accepted SHA1 passwords with an empty salt.
152    elif len(encoded) == 46 and encoded.startswith("sha1$$"):
153        algorithm = "unsalted_sha1"
154    else:
155        algorithm = encoded.split("$", 1)[0]
156    return get_hasher(algorithm)
157
158
159def mask_hash(hash, show=6, char="*"):
160    """
161    Return the given hash, with only the first ``show`` number shown. The
162    rest are masked with ``char`` for security reasons.
163    """
164    masked = hash[:show]
165    masked += char * len(hash[show:])
166    return masked
167
168
169class BasePasswordHasher:
170    """
171    Abstract base class for password hashers
172
173    When creating your own hasher, you need to override algorithm,
174    verify(), encode() and safe_summary().
175
176    PasswordHasher objects are immutable.
177    """
178
179    algorithm = None
180    library = None
181
182    def _load_library(self):
183        if self.library is not None:
184            if isinstance(self.library, (tuple, list)):
185                name, mod_path = self.library
186            else:
187                mod_path = self.library
188            try:
189                module = importlib.import_module(mod_path)
190            except ImportError as e:
191                raise ValueError(
192                    "Couldn't load %r algorithm library: %s"
193                    % (self.__class__.__name__, e)
194                )
195            return module
196        raise ValueError(
197            "Hasher %r doesn't specify a library attribute" % self.__class__.__name__
198        )
199
200    def salt(self):
201        """Generate a cryptographically secure nonce salt in ASCII."""
202        return get_random_string()
203
204    def verify(self, password, encoded):
205        """Check if the given password is correct."""
206        raise NotImplementedError(
207            "subclasses of BasePasswordHasher must provide a verify() method"
208        )
209
210    def encode(self, password, salt):
211        """
212        Create an encoded database value.
213
214        The result is normally formatted as "algorithm$salt$hash" and
215        must be fewer than 128 characters.
216        """
217        raise NotImplementedError(
218            "subclasses of BasePasswordHasher must provide an encode() method"
219        )
220
221    def safe_summary(self, encoded):
222        """
223        Return a summary of safe values.
224
225        The result is a dictionary and will be used where the password field
226        must be displayed to construct a safe representation of the password.
227        """
228        raise NotImplementedError(
229            "subclasses of BasePasswordHasher must provide a safe_summary() method"
230        )
231
232    def must_update(self, encoded):
233        return False
234
235    def harden_runtime(self, password, encoded):
236        """
237        Bridge the runtime gap between the work factor supplied in `encoded`
238        and the work factor suggested by this hasher.
239
240        Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
241        `self.iterations` is 30000, this method should run password through
242        another 10000 iterations of PBKDF2. Similar approaches should exist
243        for any hasher that has a work factor. If not, this method should be
244        defined as a no-op to silence the warning.
245        """
246        warnings.warn(
247            "subclasses of BasePasswordHasher should provide a harden_runtime() method"
248        )
249
250
251class PBKDF2PasswordHasher(BasePasswordHasher):
252    """
253    Secure password hashing using the PBKDF2 algorithm (recommended)
254
255    Configured to use PBKDF2 + HMAC + SHA256.
256    The result is a 64 byte binary string.  Iterations may be changed
257    safely but you must rename the algorithm if you change SHA256.
258    """
259
260    algorithm = "pbkdf2_sha256"
261    iterations = 216000
262    digest = hashlib.sha256
263
264    def encode(self, password, salt, iterations=None):
265        assert password is not None
266        assert salt and "$" not in salt
267        iterations = iterations or self.iterations
268        hash = pbkdf2(password, salt, iterations, digest=self.digest)
269        hash = base64.b64encode(hash).decode("ascii").strip()
270        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
271
272    def verify(self, password, encoded):
273        algorithm, iterations, salt, hash = encoded.split("$", 3)
274        assert algorithm == self.algorithm
275        encoded_2 = self.encode(password, salt, int(iterations))
276        return constant_time_compare(encoded, encoded_2)
277
278    def safe_summary(self, encoded):
279        algorithm, iterations, salt, hash = encoded.split("$", 3)
280        assert algorithm == self.algorithm
281        return {
282            _("algorithm"): algorithm,
283            _("iterations"): iterations,
284            _("salt"): mask_hash(salt),
285            _("hash"): mask_hash(hash),
286        }
287
288    def must_update(self, encoded):
289        algorithm, iterations, salt, hash = encoded.split("$", 3)
290        return int(iterations) != self.iterations
291
292    def harden_runtime(self, password, encoded):
293        algorithm, iterations, salt, hash = encoded.split("$", 3)
294        extra_iterations = self.iterations - int(iterations)
295        if extra_iterations > 0:
296            self.encode(password, salt, extra_iterations)
297
298
299class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
300    """
301    Alternate PBKDF2 hasher which uses SHA1, the default PRF
302    recommended by PKCS #5. This is compatible with other
303    implementations of PBKDF2, such as openssl's
304    PKCS5_PBKDF2_HMAC_SHA1().
305    """
306
307    algorithm = "pbkdf2_sha1"
308    digest = hashlib.sha1
309
310
311class Argon2PasswordHasher(BasePasswordHasher):
312    """
313    Secure password hashing using the argon2 algorithm.
314
315    This is the winner of the Password Hashing Competition 2013-2015
316    (https://password-hashing.net). It requires the argon2-cffi library which
317    depends on native C code and might cause portability issues.
318    """
319
320    algorithm = "argon2"
321    library = "argon2"
322
323    time_cost = 2
324    memory_cost = 512
325    parallelism = 2
326
327    def encode(self, password, salt):
328        argon2 = self._load_library()
329        data = argon2.low_level.hash_secret(
330            password.encode(),
331            salt.encode(),
332            time_cost=self.time_cost,
333            memory_cost=self.memory_cost,
334            parallelism=self.parallelism,
335            hash_len=argon2.DEFAULT_HASH_LENGTH,
336            type=argon2.low_level.Type.I,
337        )
338        return self.algorithm + data.decode("ascii")
339
340    def verify(self, password, encoded):
341        argon2 = self._load_library()
342        algorithm, rest = encoded.split("$", 1)
343        assert algorithm == self.algorithm
344        try:
345            return argon2.low_level.verify_secret(
346                ("$" + rest).encode("ascii"),
347                password.encode(),
348                type=argon2.low_level.Type.I,
349            )
350        except argon2.exceptions.VerificationError:
351            return False
352
353    def safe_summary(self, encoded):
354        (
355            algorithm,
356            variety,
357            version,
358            time_cost,
359            memory_cost,
360            parallelism,
361            salt,
362            data,
363        ) = self._decode(encoded)
364        assert algorithm == self.algorithm
365        return {
366            _("algorithm"): algorithm,
367            _("variety"): variety,
368            _("version"): version,
369            _("memory cost"): memory_cost,
370            _("time cost"): time_cost,
371            _("parallelism"): parallelism,
372            _("salt"): mask_hash(salt),
373            _("hash"): mask_hash(data),
374        }
375
376    def must_update(self, encoded):
377        (
378            algorithm,
379            variety,
380            version,
381            time_cost,
382            memory_cost,
383            parallelism,
384            salt,
385            data,
386        ) = self._decode(encoded)
387        assert algorithm == self.algorithm
388        argon2 = self._load_library()
389        return (
390            argon2.low_level.ARGON2_VERSION != version
391            or self.time_cost != time_cost
392            or self.memory_cost != memory_cost
393            or self.parallelism != parallelism
394        )
395
396    def harden_runtime(self, password, encoded):
397        # The runtime for Argon2 is too complicated to implement a sensible
398        # hardening algorithm.
399        pass
400
401    def _decode(self, encoded):
402        """
403        Split an encoded hash and return: (
404            algorithm, variety, version, time_cost, memory_cost,
405            parallelism, salt, data,
406        ).
407        """
408        bits = encoded.split("$")
409        if len(bits) == 5:
410            # Argon2 < 1.3
411            algorithm, variety, raw_params, salt, data = bits
412            version = 0x10
413        else:
414            assert len(bits) == 6
415            algorithm, variety, raw_version, raw_params, salt, data = bits
416            assert raw_version.startswith("v=")
417            version = int(raw_version[len("v=") :])
418        params = dict(bit.split("=", 1) for bit in raw_params.split(","))
419        assert len(params) == 3 and all(x in params for x in ("t", "m", "p"))
420        time_cost = int(params["t"])
421        memory_cost = int(params["m"])
422        parallelism = int(params["p"])
423        return (
424            algorithm,
425            variety,
426            version,
427            time_cost,
428            memory_cost,
429            parallelism,
430            salt,
431            data,
432        )
433
434
435class BCryptSHA256PasswordHasher(BasePasswordHasher):
436    """
437    Secure password hashing using the bcrypt algorithm (recommended)
438
439    This is considered by many to be the most secure algorithm but you
440    must first install the bcrypt library.  Please be warned that
441    this library depends on native C code and might cause portability
442    issues.
443    """
444
445    algorithm = "bcrypt_sha256"
446    digest = hashlib.sha256
447    library = ("bcrypt", "bcrypt")
448    rounds = 12
449
450    def salt(self):
451        bcrypt = self._load_library()
452        return bcrypt.gensalt(self.rounds)
453
454    def encode(self, password, salt):
455        bcrypt = self._load_library()
456        password = password.encode()
457        # Hash the password prior to using bcrypt to prevent password
458        # truncation as described in #20138.
459        if self.digest is not None:
460            # Use binascii.hexlify() because a hex encoded bytestring is str.
461            password = binascii.hexlify(self.digest(password).digest())
462
463        data = bcrypt.hashpw(password, salt)
464        return "%s$%s" % (self.algorithm, data.decode("ascii"))
465
466    def verify(self, password, encoded):
467        algorithm, data = encoded.split("$", 1)
468        assert algorithm == self.algorithm
469        encoded_2 = self.encode(password, data.encode("ascii"))
470        return constant_time_compare(encoded, encoded_2)
471
472    def safe_summary(self, encoded):
473        algorithm, empty, algostr, work_factor, data = encoded.split("$", 4)
474        assert algorithm == self.algorithm
475        salt, checksum = data[:22], data[22:]
476        return {
477            _("algorithm"): algorithm,
478            _("work factor"): work_factor,
479            _("salt"): mask_hash(salt),
480            _("checksum"): mask_hash(checksum),
481        }
482
483    def must_update(self, encoded):
484        algorithm, empty, algostr, rounds, data = encoded.split("$", 4)
485        return int(rounds) != self.rounds
486
487    def harden_runtime(self, password, encoded):
488        _, data = encoded.split("$", 1)
489        salt = data[:29]  # Length of the salt in bcrypt.
490        rounds = data.split("$")[2]
491        # work factor is logarithmic, adding one doubles the load.
492        diff = 2 ** (self.rounds - int(rounds)) - 1
493        while diff > 0:
494            self.encode(password, salt.encode("ascii"))
495            diff -= 1
496
497
498class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
499    """
500    Secure password hashing using the bcrypt algorithm
501
502    This is considered by many to be the most secure algorithm but you
503    must first install the bcrypt library.  Please be warned that
504    this library depends on native C code and might cause portability
505    issues.
506
507    This hasher does not first hash the password which means it is subject to
508    bcrypt's 72 bytes password truncation. Most use cases should prefer the
509    BCryptSHA256PasswordHasher.
510    """
511
512    algorithm = "bcrypt"
513    digest = None
514
515
516class SHA1PasswordHasher(BasePasswordHasher):
517    """
518    The SHA1 password hashing algorithm (not recommended)
519    """
520
521    algorithm = "sha1"
522
523    def encode(self, password, salt):
524        assert password is not None
525        assert salt and "$" not in salt
526        hash = hashlib.sha1((salt + password).encode()).hexdigest()
527        return "%s$%s$%s" % (self.algorithm, salt, hash)
528
529    def verify(self, password, encoded):
530        algorithm, salt, hash = encoded.split("$", 2)
531        assert algorithm == self.algorithm
532        encoded_2 = self.encode(password, salt)
533        return constant_time_compare(encoded, encoded_2)
534
535    def safe_summary(self, encoded):
536        algorithm, salt, hash = encoded.split("$", 2)
537        assert algorithm == self.algorithm
538        return {
539            _("algorithm"): algorithm,
540            _("salt"): mask_hash(salt, show=2),
541            _("hash"): mask_hash(hash),
542        }
543
544    def harden_runtime(self, password, encoded):
545        pass
546
547
548class MD5PasswordHasher(BasePasswordHasher):
549    """
550    The Salted MD5 password hashing algorithm (not recommended)
551    """
552
553    algorithm = "md5"
554
555    def encode(self, password, salt):
556        assert password is not None
557        assert salt and "$" not in salt
558        hash = hashlib.md5((salt + password).encode()).hexdigest()
559        return "%s$%s$%s" % (self.algorithm, salt, hash)
560
561    def verify(self, password, encoded):
562        algorithm, salt, hash = encoded.split("$", 2)
563        assert algorithm == self.algorithm
564        encoded_2 = self.encode(password, salt)
565        return constant_time_compare(encoded, encoded_2)
566
567    def safe_summary(self, encoded):
568        algorithm, salt, hash = encoded.split("$", 2)
569        assert algorithm == self.algorithm
570        return {
571            _("algorithm"): algorithm,
572            _("salt"): mask_hash(salt, show=2),
573            _("hash"): mask_hash(hash),
574        }
575
576    def harden_runtime(self, password, encoded):
577        pass
578
579
580class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
581    """
582    Very insecure algorithm that you should *never* use; store SHA1 hashes
583    with an empty salt.
584
585    This class is implemented because Django used to accept such password
586    hashes. Some older Django installs still have these values lingering
587    around so we need to handle and upgrade them properly.
588    """
589
590    algorithm = "unsalted_sha1"
591
592    def salt(self):
593        return ""
594
595    def encode(self, password, salt):
596        assert salt == ""
597        hash = hashlib.sha1(password.encode()).hexdigest()
598        return "sha1$$%s" % hash
599
600    def verify(self, password, encoded):
601        encoded_2 = self.encode(password, "")
602        return constant_time_compare(encoded, encoded_2)
603
604    def safe_summary(self, encoded):
605        assert encoded.startswith("sha1$$")
606        hash = encoded[6:]
607        return {
608            _("algorithm"): self.algorithm,
609            _("hash"): mask_hash(hash),
610        }
611
612    def harden_runtime(self, password, encoded):
613        pass
614
615
616class UnsaltedMD5PasswordHasher(BasePasswordHasher):
617    """
618    Incredibly insecure algorithm that you should *never* use; stores unsalted
619    MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
620    empty salt.
621
622    This class is implemented because Django used to store passwords this way
623    and to accept such password hashes. Some older Django installs still have
624    these values lingering around so we need to handle and upgrade them
625    properly.
626    """
627
628    algorithm = "unsalted_md5"
629
630    def salt(self):
631        return ""
632
633    def encode(self, password, salt):
634        assert salt == ""
635        return hashlib.md5(password.encode()).hexdigest()
636
637    def verify(self, password, encoded):
638        if len(encoded) == 37 and encoded.startswith("md5$$"):
639            encoded = encoded[5:]
640        encoded_2 = self.encode(password, "")
641        return constant_time_compare(encoded, encoded_2)
642
643    def safe_summary(self, encoded):
644        return {
645            _("algorithm"): self.algorithm,
646            _("hash"): mask_hash(encoded, show=3),
647        }
648
649    def harden_runtime(self, password, encoded):
650        pass
651
652
653class CryptPasswordHasher(BasePasswordHasher):
654    """
655    Password hashing using UNIX crypt (not recommended)
656
657    The crypt module is not supported on all platforms.
658    """
659
660    algorithm = "crypt"
661    library = "crypt"
662
663    def salt(self):
664        return get_random_string(2)
665
666    def encode(self, password, salt):
667        crypt = self._load_library()
668        assert len(salt) == 2
669        data = crypt.crypt(password, salt)
670        assert data is not None  # A platform like OpenBSD with a dummy crypt module.
671        # we don't need to store the salt, but Django used to do this
672        return "%s$%s$%s" % (self.algorithm, "", data)
673
674    def verify(self, password, encoded):
675        crypt = self._load_library()
676        algorithm, salt, data = encoded.split("$", 2)
677        assert algorithm == self.algorithm
678        return constant_time_compare(data, crypt.crypt(password, data))
679
680    def safe_summary(self, encoded):
681        algorithm, salt, data = encoded.split("$", 2)
682        assert algorithm == self.algorithm
683        return {
684            _("algorithm"): algorithm,
685            _("salt"): salt,
686            _("hash"): mask_hash(data, show=3),
687        }
688
689    def harden_runtime(self, password, encoded):
690        pass

django/contrib/admin/options.py

   1import copy
   2import json
   3import operator
   4import re
   5from functools import partial, reduce, update_wrapper
   6from urllib.parse import quote as urlquote
   7
   8from django import forms
   9from django.conf import settings
  10from django.contrib import messages
  11from django.contrib.admin import helpers, widgets
  12from django.contrib.admin.checks import (
  13    BaseModelAdminChecks,
  14    InlineModelAdminChecks,
  15    ModelAdminChecks,
  16)
  17from django.contrib.admin.exceptions import DisallowedModelAdminToField
  18from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
  19from django.contrib.admin.utils import (
  20    NestedObjects,
  21    construct_change_message,
  22    flatten_fieldsets,
  23    get_deleted_objects,
  24    lookup_needs_distinct,
  25    model_format_dict,
  26    model_ngettext,
  27    quote,
  28    unquote,
  29)
  30from django.contrib.admin.views.autocomplete import AutocompleteJsonView
  31from django.contrib.admin.widgets import (
  32    AutocompleteSelect,
  33    AutocompleteSelectMultiple,
  34)
  35from django.contrib.auth import get_permission_codename
  36from django.core.exceptions import (
  37    FieldDoesNotExist,
  38    FieldError,
  39    PermissionDenied,
  40    ValidationError,
  41)
  42from django.core.paginator import Paginator
  43from django.db import models, router, transaction
  44from django.db.models.constants import LOOKUP_SEP
  45from django.db.models.fields import BLANK_CHOICE_DASH
  46from django.forms.formsets import DELETION_FIELD_NAME, all_valid
  47from django.forms.models import (
  48    BaseInlineFormSet,
  49    inlineformset_factory,
  50    modelform_defines_fields,
  51    modelform_factory,
  52    modelformset_factory,
  53)
  54from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
  55from django.http import HttpResponseRedirect
  56from django.http.response import HttpResponseBase
  57from django.template.response import SimpleTemplateResponse, TemplateResponse
  58from django.urls import reverse
  59from django.utils.decorators import method_decorator
  60from django.utils.html import format_html
  61from django.utils.http import urlencode
  62from django.utils.safestring import mark_safe
  63from django.utils.text import capfirst, format_lazy, get_text_list
  64from django.utils.translation import gettext as _, ngettext
  65from django.views.decorators.csrf import csrf_protect
  66from django.views.generic import RedirectView
  67
  68IS_POPUP_VAR = "_popup"
  69TO_FIELD_VAR = "_to_field"
  70
  71
  72HORIZONTAL, VERTICAL = 1, 2
  73
  74
  75def get_content_type_for_model(obj):
  76    # Since this module gets imported in the application's root package,
  77    # it cannot import models from other applications at the module level.
  78    from django.contrib.contenttypes.models import ContentType
  79
  80    return ContentType.objects.get_for_model(obj, for_concrete_model=False)
  81
  82
  83def get_ul_class(radio_style):
  84    return "radiolist" if radio_style == VERTICAL else "radiolist inline"
  85
  86
  87class IncorrectLookupParameters(Exception):
  88    pass
  89
  90
  91# Defaults for formfield_overrides. ModelAdmin subclasses can change this
  92# by adding to ModelAdmin.formfield_overrides.
  93
  94FORMFIELD_FOR_DBFIELD_DEFAULTS = {
  95    models.DateTimeField: {
  96        "form_class": forms.SplitDateTimeField,
  97        "widget": widgets.AdminSplitDateTime,
  98    },
  99    models.DateField: {"widget": widgets.AdminDateWidget},
 100    models.TimeField: {"widget": widgets.AdminTimeWidget},
 101    models.TextField: {"widget": widgets.AdminTextareaWidget},
 102    models.URLField: {"widget": widgets.AdminURLFieldWidget},
 103    models.IntegerField: {"widget": widgets.AdminIntegerFieldWidget},
 104    models.BigIntegerField: {"widget": widgets.AdminBigIntegerFieldWidget},
 105    models.CharField: {"widget": widgets.AdminTextInputWidget},
 106    models.ImageField: {"widget": widgets.AdminFileWidget},
 107    models.FileField: {"widget": widgets.AdminFileWidget},
 108    models.EmailField: {"widget": widgets.AdminEmailInputWidget},
 109    models.UUIDField: {"widget": widgets.AdminUUIDInputWidget},
 110}
 111
 112csrf_protect_m = method_decorator(csrf_protect)
 113
 114
 115class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
 116    """Functionality common to both ModelAdmin and InlineAdmin."""
 117
 118    autocomplete_fields = ()
 119    raw_id_fields = ()
 120    fields = None
 121    exclude = None
 122    fieldsets = None
 123    form = forms.ModelForm
 124    filter_vertical = ()
 125    filter_horizontal = ()
 126    radio_fields = {}
 127    prepopulated_fields = {}
 128    formfield_overrides = {}
 129    readonly_fields = ()
 130    ordering = None
 131    sortable_by = None
 132    view_on_site = True
 133    show_full_result_count = True
 134    checks_class = BaseModelAdminChecks
 135
 136    def check(self, **kwargs):
 137        return self.checks_class().check(self, **kwargs)
 138
 139    def __init__(self):
 140        # Merge FORMFIELD_FOR_DBFIELD_DEFAULTS with the formfield_overrides
 141        # rather than simply overwriting.
 142        overrides = copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS)
 143        for k, v in self.formfield_overrides.items():
 144            overrides.setdefault(k, {}).update(v)
 145        self.formfield_overrides = overrides
 146
 147    def formfield_for_dbfield(self, db_field, request, **kwargs):
 148        """
 149        Hook for specifying the form Field instance for a given database Field
 150        instance.
 151
 152        If kwargs are given, they're passed to the form Field's constructor.
 153        """
 154        # If the field specifies choices, we don't need to look for special
 155        # admin widgets - we just need to use a select widget of some kind.
 156        if db_field.choices:
 157            return self.formfield_for_choice_field(db_field, request, **kwargs)
 158
 159        # ForeignKey or ManyToManyFields
 160        if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
 161            # Combine the field kwargs with any options for formfield_overrides.
 162            # Make sure the passed in **kwargs override anything in
 163            # formfield_overrides because **kwargs is more specific, and should
 164            # always win.
 165            if db_field.__class__ in self.formfield_overrides:
 166                kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs}
 167
 168            # Get the correct formfield.
 169            if isinstance(db_field, models.ForeignKey):
 170                formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
 171            elif isinstance(db_field, models.ManyToManyField):
 172                formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
 173
 174            # For non-raw_id fields, wrap the widget with a wrapper that adds
 175            # extra HTML -- the "add other" interface -- to the end of the
 176            # rendered output. formfield can be None if it came from a
 177            # OneToOneField with parent_link=True or a M2M intermediary.
 178            if formfield and db_field.name not in self.raw_id_fields:
 179                related_modeladmin = self.admin_site._registry.get(
 180                    db_field.remote_field.model
 181                )
 182                wrapper_kwargs = {}
 183                if related_modeladmin:
 184                    wrapper_kwargs.update(
 185                        can_add_related=related_modeladmin.has_add_permission(request),
 186                        can_change_related=related_modeladmin.has_change_permission(
 187                            request
 188                        ),
 189                        can_delete_related=related_modeladmin.has_delete_permission(
 190                            request
 191                        ),
 192                        can_view_related=related_modeladmin.has_view_permission(
 193                            request
 194                        ),
 195                    )
 196                formfield.widget = widgets.RelatedFieldWidgetWrapper(
 197                    formfield.widget,
 198                    db_field.remote_field,
 199                    self.admin_site,
 200                    **wrapper_kwargs
 201                )
 202
 203            return formfield
 204
 205        # If we've got overrides for the formfield defined, use 'em. **kwargs
 206        # passed to formfield_for_dbfield override the defaults.
 207        for klass in db_field.__class__.mro():
 208            if klass in self.formfield_overrides:
 209                kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs}
 210                return db_field.formfield(**kwargs)
 211
 212        # For any other type of field, just call its formfield() method.
 213        return db_field.formfield(**kwargs)
 214
 215    def formfield_for_choice_field(self, db_field, request, **kwargs):
 216        """
 217        Get a form Field for a database Field that has declared choices.
 218        """
 219        # If the field is named as a radio_field, use a RadioSelect
 220        if db_field.name in self.radio_fields:
 221            # Avoid stomping on custom widget/choices arguments.
 222            if "widget" not in kwargs:
 223                kwargs["widget"] = widgets.AdminRadioSelect(
 224                    attrs={"class": get_ul_class(self.radio_fields[db_field.name]),}
 225                )
 226            if "choices" not in kwargs:
 227                kwargs["choices"] = db_field.get_choices(
 228                    include_blank=db_field.blank, blank_choice=[("", _("None"))]
 229                )
 230        return db_field.formfield(**kwargs)
 231
 232    def get_field_queryset(self, db, db_field, request):
 233        """
 234        If the ModelAdmin specifies ordering, the queryset should respect that
 235        ordering.  Otherwise don't specify the queryset, let the field decide
 236        (return None in that case).
 237        """
 238        related_admin = self.admin_site._registry.get(db_field.remote_field.model)
 239        if related_admin is not None:
 240            ordering = related_admin.get_ordering(request)
 241            if ordering is not None and ordering != ():
 242                return db_field.remote_field.model._default_manager.using(db).order_by(
 243                    *ordering
 244                )
 245        return None
 246
 247    def formfield_for_foreignkey(self, db_field, request, **kwargs):
 248        """
 249        Get a form Field for a ForeignKey.
 250        """
 251        db = kwargs.get("using")
 252
 253        if "widget" not in kwargs:
 254            if db_field.name in self.get_autocomplete_fields(request):
 255                kwargs["widget"] = AutocompleteSelect(
 256                    db_field.remote_field, self.admin_site, using=db
 257                )
 258            elif db_field.name in self.raw_id_fields:
 259                kwargs["widget"] = widgets.ForeignKeyRawIdWidget(
 260                    db_field.remote_field, self.admin_site, using=db
 261                )
 262            elif db_field.name in self.radio_fields:
 263                kwargs["widget"] = widgets.AdminRadioSelect(
 264                    attrs={"class": get_ul_class(self.radio_fields[db_field.name]),}
 265                )
 266                kwargs["empty_label"] = _("None") if db_field.blank else None
 267
 268        if "queryset" not in kwargs:
 269            queryset = self.get_field_queryset(db, db_field, request)
 270            if queryset is not None:
 271                kwargs["queryset"] = queryset
 272
 273        return db_field.formfield(**kwargs)
 274
 275    def formfield_for_manytomany(self, db_field, request, **kwargs):
 276        """
 277        Get a form Field for a ManyToManyField.
 278        """
 279        # If it uses an intermediary model that isn't auto created, don't show
 280        # a field in admin.
 281        if not db_field.remote_field.through._meta.auto_created:
 282            return None
 283        db = kwargs.get("using")
 284
 285        autocomplete_fields = self.get_autocomplete_fields(request)
 286        if db_field.name in autocomplete_fields:
 287            kwargs["widget"] = AutocompleteSelectMultiple(
 288                db_field.remote_field, self.admin_site, using=db
 289            )
 290        elif db_field.name in self.raw_id_fields:
 291            kwargs["widget"] = widgets.ManyToManyRawIdWidget(
 292                db_field.remote_field, self.admin_site, using=db
 293            )
 294        elif db_field.name in [*self.filter_vertical, *self.filter_horizontal]:
 295            kwargs["widget"] = widgets.FilteredSelectMultiple(
 296                db_field.verbose_name, db_field.name in self.filter_vertical
 297            )
 298
 299        if "queryset" not in kwargs:
 300            queryset = self.get_field_queryset(db, db_field, request)
 301            if queryset is not None:
 302                kwargs["queryset"] = queryset
 303
 304        form_field = db_field.formfield(**kwargs)
 305        if isinstance(form_field.widget, SelectMultiple) and not isinstance(
 306            form_field.widget, (CheckboxSelectMultiple, AutocompleteSelectMultiple)
 307        ):
 308            msg = _(
 309                "Hold down “Control”, or “Command” on a Mac, to select more than one."
 310            )
 311            help_text = form_field.help_text
 312            form_field.help_text = (
 313                format_lazy("{} {}", help_text, msg) if help_text else msg
 314            )
 315        return form_field
 316
 317    def get_autocomplete_fields(self, request):
 318        """
 319        Return a list of ForeignKey and/or ManyToMany fields which should use
 320        an autocomplete widget.
 321        """
 322        return self.autocomplete_fields
 323
 324    def get_view_on_site_url(self, obj=None):
 325        if obj is None or not self.view_on_site:
 326            return None
 327
 328        if callable(self.view_on_site):
 329            return self.view_on_site(obj)
 330        elif self.view_on_site and hasattr(obj, "get_absolute_url"):
 331            # use the ContentType lookup if view_on_site is True
 332            return reverse(
 333                "admin:view_on_site",
 334                kwargs={
 335                    "content_type_id": get_content_type_for_model(obj).pk,
 336                    "object_id": obj.pk,
 337                },
 338            )
 339
 340    def get_empty_value_display(self):
 341        """
 342        Return the empty_value_display set on ModelAdmin or AdminSite.
 343        """
 344        try:
 345            return mark_safe(self.empty_value_display)
 346        except AttributeError:
 347            return mark_safe(self.admin_site.empty_value_display)
 348
 349    def get_exclude(self, request, obj=None):
 350        """
 351        Hook for specifying exclude.
 352        """
 353        return self.exclude
 354
 355    def get_fields(self, request, obj=None):
 356        """
 357        Hook for specifying fields.
 358        """
 359        if self.fields:
 360            return self.fields
 361        # _get_form_for_get_fields() is implemented in subclasses.
 362        form = self._get_form_for_get_fields(request, obj)
 363        return [*form.base_fields, *self.get_readonly_fields(request, obj)]
 364
 365    def get_fieldsets(self, request, obj=None):
 366        """
 367        Hook for specifying fieldsets.
 368        """
 369        if self.fieldsets:
 370            return self.fieldsets
 371        return [(None, {"fields": self.get_fields(request, obj)})]
 372
 373    def get_inlines(self, request, obj):
 374        """Hook for specifying custom inlines."""
 375        return self.inlines
 376
 377    def get_ordering(self, request):
 378        """
 379        Hook for specifying field ordering.
 380        """
 381        return self.ordering or ()  # otherwise we might try to *None, which is bad ;)
 382
 383    def get_readonly_fields(self, request, obj=None):
 384        """
 385        Hook for specifying custom readonly fields.
 386        """
 387        return self.readonly_fields
 388
 389    def get_prepopulated_fields(self, request, obj=None):
 390        """
 391        Hook for specifying custom prepopulated fields.
 392        """
 393        return self.prepopulated_fields
 394
 395    def get_queryset(self, request):
 396        """
 397        Return a QuerySet of all model instances that can be edited by the
 398        admin site. This is used by changelist_view.
 399        """
 400        qs = self.model._default_manager.get_queryset()
 401        # TODO: this should be handled by some parameter to the ChangeList.
 402        ordering = self.get_ordering(request)
 403        if ordering:
 404            qs = qs.order_by(*ordering)
 405        return qs
 406
 407    def get_sortable_by(self, request):
 408        """Hook for specifying which fields can be sorted in the changelist."""
 409        return (
 410            self.sortable_by
 411            if self.sortable_by is not None
 412            else self.get_list_display(request)
 413        )
 414
 415    def lookup_allowed(self, lookup, value):
 416        from django.contrib.admin.filters import SimpleListFilter
 417
 418        model = self.model
 419        # Check FKey lookups that are allowed, so that popups produced by
 420        # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
 421        # are allowed to work.
 422        for fk_lookup in model._meta.related_fkey_lookups:
 423            # As ``limit_choices_to`` can be a callable, invoke it here.
 424            if callable(fk_lookup):
 425                fk_lookup = fk_lookup()
 426            if (lookup, value) in widgets.url_params_from_lookup_dict(
 427                fk_lookup
 428            ).items():
 429                return True
 430
 431        relation_parts = []
 432        prev_field = None
 433        for part in lookup.split(LOOKUP_SEP):
 434            try:
 435                field = model._meta.get_field(part)
 436            except FieldDoesNotExist:
 437                # Lookups on nonexistent fields are ok, since they're ignored
 438                # later.
 439                break
 440            # It is allowed to filter on values that would be found from local
 441            # model anyways. For example, if you filter on employee__department__id,
 442            # then the id value would be found already from employee__department_id.
 443            if not prev_field or (
 444                prev_field.is_relation
 445                and field not in prev_field.get_path_info()[-1].target_fields
 446            ):
 447                relation_parts.append(part)
 448            if not getattr(field, "get_path_info", None):
 449                # This is not a relational field, so further parts
 450                # must be transforms.
 451                break
 452            prev_field = field
 453            model = field.get_path_info()[-1].to_opts.model
 454
 455        if len(relation_parts) <= 1:
 456            # Either a local field filter, or no fields at all.
 457            return True
 458        valid_lookups = {self.date_hierarchy}
 459        for filter_item in self.list_filter:
 460            if isinstance(filter_item, type) and issubclass(
 461                filter_item, SimpleListFilter
 462            ):
 463                valid_lookups.add(filter_item.parameter_name)
 464            elif isinstance(filter_item, (list, tuple)):
 465                valid_lookups.add(filter_item[0])
 466            else:
 467                valid_lookups.add(filter_item)
 468
 469        # Is it a valid relational lookup?
 470        return not {
 471            LOOKUP_SEP.join(relation_parts),
 472            LOOKUP_SEP.join(relation_parts + [part]),
 473        }.isdisjoint(valid_lookups)
 474
 475    def to_field_allowed(self, request, to_field):
 476        """
 477        Return True if the model associated with this admin should be
 478        allowed to be referenced by the specified field.
 479        """
 480        opts = self.model._meta
 481
 482        try:
 483            field = opts.get_field(to_field)
 484        except FieldDoesNotExist:
 485            return False
 486
 487        # Always allow referencing the primary key since it's already possible
 488        # to get this information from the change view URL.
 489        if field.primary_key:
 490            return True
 491
 492        # Allow reverse relationships to models defining m2m fields if they
 493        # target the specified field.
 494        for many_to_many in opts.many_to_many:
 495            if many_to_many.m2m_target_field_name() == to_field:
 496                return True
 497
 498        # Make sure at least one of the models registered for this site
 499        # references this field through a FK or a M2M relationship.
 500        registered_models = set()
 501        for model, admin in self.admin_site._registry.items():
 502            registered_models.add(model)
 503            for inline in admin.inlines:
 504                registered_models.add(inline.model)
 505
 506        related_objects = (
 507            f
 508            for f in opts.get_fields(include_hidden=True)
 509            if (f.auto_created and not f.concrete)
 510        )
 511        for related_object in related_objects:
 512            related_model = related_object.related_model
 513            remote_field = related_object.field.remote_field
 514            if (
 515                any(issubclass(model, related_model) for model in registered_models)
 516                and hasattr(remote_field, "get_related_field")
 517                and remote_field.get_related_field() == field
 518            ):
 519                return True
 520
 521        return False
 522
 523    def has_add_permission(self, request):
 524        """
 525        Return True if the given request has permission to add an object.
 526        Can be overridden by the user in subclasses.
 527        """
 528        opts = self.opts
 529        codename = get_permission_codename("add", opts)
 530        return request.user.has_perm("%s.%s" % (opts.app_label, codename))
 531
 532    def has_change_permission(self, request, obj=None):
 533        """
 534        Return True if the given request has permission to change the given
 535        Django model instance, the default implementation doesn't examine the
 536        `obj` parameter.
 537
 538        Can be overridden by the user in subclasses. In such case it should
 539        return True if the given request has permission to change the `obj`
 540        model instance. If `obj` is None, this should return True if the given
 541        request has permission to change *any* object of the given type.
 542        """
 543        opts = self.opts
 544        codename = get_permission_codename("change", opts)
 545        return request.user.has_perm("%s.%s" % (opts.app_label, codename))
 546
 547    def has_delete_permission(self, request, obj=None):
 548        """
 549        Return True if the given request has permission to change the given
 550        Django model instance, the default implementation doesn't examine the
 551        `obj` parameter.
 552
 553        Can be overridden by the user in subclasses. In such case it should
 554        return True if the given request has permission to delete the `obj`
 555        model instance. If `obj` is None, this should return True if the given
 556        request has permission to delete *any* object of the given type.
 557        """
 558        opts = self.opts
 559        codename = get_permission_codename("delete", opts)
 560        return request.user.has_perm("%s.%s" % (opts.app_label, codename))
 561
 562    def has_view_permission(self, request, obj=None):
 563        """
 564        Return True if the given request has permission to view the given
 565        Django model instance. The default implementation doesn't examine the
 566        `obj` parameter.
 567
 568        If overridden by the user in subclasses, it should return True if the
 569        given request has permission to view the `obj` model instance. If `obj`
 570        is None, it should return True if the request has permission to view
 571        any object of the given type.
 572        """
 573        opts = self.opts
 574        codename_view = get_permission_codename("view", opts)
 575        codename_change = get_permission_codename("change", opts)
 576        return request.user.has_perm(
 577            "%s.%s" % (opts.app_label, codename_view)
 578        ) or request.user.has_perm("%s.%s" % (opts.app_label, codename_change))
 579
 580    def has_view_or_change_permission(self, request, obj=None):
 581        return self.has_view_permission(request, obj) or self.has_change_permission(
 582            request, obj
 583        )
 584
 585    def has_module_permission(self, request):
 586        """
 587        Return True if the given request has any permission in the given
 588        app label.
 589
 590        Can be overridden by the user in subclasses. In such case it should
 591        return True if the given request has permission to view the module on
 592        the admin index page and access the module's index page. Overriding it
 593        does not restrict access to the add, change or delete views. Use
 594        `ModelAdmin.has_(add|change|delete)_permission` for that.
 595        """
 596        return request.user.has_module_perms(self.opts.app_label)
 597
 598
 599class ModelAdmin(BaseModelAdmin):
 600    """Encapsulate all admin options and functionality for a given model."""
 601
 602    list_display = ("__str__",)
 603    list_display_links = ()
 604    list_filter = ()
 605    list_select_related = False
 606    list_per_page = 100
 607    list_max_show_all = 200
 608    list_editable = ()
 609    search_fields = ()
 610    date_hierarchy = None
 611    save_as = False
 612    save_as_continue = True
 613    save_on_top = False
 614    paginator = Paginator
 615    preserve_filters = True
 616    inlines = []
 617
 618    # Custom templates (designed to be over-ridden in subclasses)
 619    add_form_template = None
 620    change_form_template = None
 621    change_list_template = None
 622    delete_confirmation_template = None
 623    delete_selected_confirmation_template = None
 624    object_history_template = None
 625    popup_response_template = None
 626
 627    # Actions
 628    actions = []
 629    action_form = helpers.ActionForm
 630    actions_on_top = True
 631    actions_on_bottom = False
 632    actions_selection_counter = True
 633    checks_class = ModelAdminChecks
 634
 635    def __init__(self, model, admin_site):
 636        self.model = model
 637        self.opts = model._meta
 638        self.admin_site = admin_site
 639        super().__init__()
 640
 641    def __str__(self):
 642        return "%s.%s" % (self.model._meta.app_label, self.__class__.__name__)
 643
 644    def get_inline_instances(self, request, obj=None):
 645        inline_instances = []
 646        for inline_class in self.get_inlines(request, obj):
 647            inline = inline_class(self.model, self.admin_site)
 648            if request:
 649                if not (
 650                    inline.has_view_or_change_permission(request, obj)
 651                    or inline.has_add_permission(request, obj)
 652                    or inline.has_delete_permission(request, obj)
 653                ):
 654                    continue
 655                if not inline.has_add_permission(request, obj):
 656                    inline.max_num = 0
 657            inline_instances.append(inline)
 658
 659        return inline_instances
 660
 661    def get_urls(self):
 662        from django.urls import path
 663
 664        def wrap(view):
 665            def wrapper(*args, **kwargs):
 666                return self.admin_site.admin_view(view)(*args, **kwargs)
 667
 668            wrapper.model_admin = self
 669            return update_wrapper(wrapper, view)
 670
 671        info = self.model._meta.app_label, self.model._meta.model_name
 672
 673        return [
 674            path("", wrap(self.changelist_view), name="%s_%s_changelist" % info),
 675            path("add/", wrap(self.add_view), name="%s_%s_add" % info),
 676            path(
 677                "autocomplete/",
 678                wrap(self.autocomplete_view),
 679                name="%s_%s_autocomplete" % info,
 680            ),
 681            path(
 682                "<path:object_id>/history/",
 683                wrap(self.history_view),
 684                name="%s_%s_history" % info,
 685            ),
 686            path(
 687                "<path:object_id>/delete/",
 688                wrap(self.delete_view),
 689                name="%s_%s_delete" % info,
 690            ),
 691            path(
 692                "<path:object_id>/change/",
 693                wrap(self.change_view),
 694                name="%s_%s_change" % info,
 695            ),
 696            # For backwards compatibility (was the change url before 1.9)
 697            path(
 698                "<path:object_id>/",
 699                wrap(
 700                    RedirectView.as_view(
 701                        pattern_name="%s:%s_%s_change"
 702                        % ((self.admin_site.name,) + info)
 703                    )
 704                ),
 705            ),
 706        ]
 707
 708    @property
 709    def urls(self):
 710        return self.get_urls()
 711
 712    @property
 713    def media(self):
 714        extra = "" if settings.DEBUG else ".min"
 715        js = [
 716            "vendor/jquery/jquery%s.js" % extra,
 717            "jquery.init.js",
 718            "core.js",
 719            "admin/RelatedObjectLookups.js",
 720            "actions%s.js" % extra,
 721            "urlify.js",
 722            "prepopulate%s.js" % extra,
 723            "vendor/xregexp/xregexp%s.js" % extra,
 724        ]
 725        return forms.Media(js=["admin/js/%s" % url for url in js])
 726
 727    def get_model_perms(self, request):
 728        """
 729        Return a dict of all perms for this model. This dict has the keys
 730        ``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
 731        for each of those actions.
 732        """
 733        return {
 734            "add": self.has_add_permission(request),
 735            "change": self.has_change_permission(request),
 736            "delete": self.has_delete_permission(request),
 737            "view": self.has_view_permission(request),
 738        }
 739
 740    def _get_form_for_get_fields(self, request, obj):
 741        return self.get_form(request, obj, fields=None)
 742
 743    def get_form(self, request, obj=None, change=False, **kwargs):
 744        """
 745        Return a Form class for use in the admin add view. This is used by
 746        add_view and change_view.
 747        """
 748        if "fields" in kwargs:
 749            fields = kwargs.pop("fields")
 750        else:
 751            fields = flatten_fieldsets(self.get_fieldsets(request, obj))
 752        excluded = self.get_exclude(request, obj)
 753        exclude = [] if excluded is None else list(excluded)
 754        readonly_fields = self.get_readonly_fields(request, obj)
 755        exclude.extend(readonly_fields)
 756        # Exclude all fields if it's a change form and the user doesn't have
 757        # the change permission.
 758        if (
 759            change
 760            and hasattr(request, "user")
 761            and not self.has_change_permission(request, obj)
 762        ):
 763            exclude.extend(fields)
 764        if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
 765            # Take the custom ModelForm's Meta.exclude into account only if the
 766            # ModelAdmin doesn't define its own.
 767            exclude.extend(self.form._meta.exclude)
 768        # if exclude is an empty list we pass None to be consistent with the
 769        # default on modelform_factory
 770        exclude = exclude or None
 771
 772        # Remove declared form fields which are in readonly_fields.
 773        new_attrs = dict.fromkeys(
 774            f for f in readonly_fields if f in self.form.declared_fields
 775        )
 776        form = type(self.form.__name__, (self.form,), new_attrs)
 777
 778        defaults = {
 779            "form": form,
 780            "fields": fields,
 781            "exclude": exclude,
 782            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
 783            **kwargs,
 784        }
 785
 786        if defaults["fields"] is None and not modelform_defines_fields(
 787            defaults["form"]
 788        ):
 789            defaults["fields"] = forms.ALL_FIELDS
 790
 791        try:
 792            return modelform_factory(self.model, **defaults)
 793        except FieldError as e:
 794            raise FieldError(
 795                "%s. Check fields/fieldsets/exclude attributes of class %s."
 796                % (e, self.__class__.__name__)
 797            )
 798
 799    def get_changelist(self, request, **kwargs):
 800        """
 801        Return the ChangeList class for use on the changelist page.
 802        """
 803        from django.contrib.admin.views.main import ChangeList
 804
 805        return ChangeList
 806
 807    def get_changelist_instance(self, request):
 808        """
 809        Return a `ChangeList` instance based on `request`. May raise
 810        `IncorrectLookupParameters`.
 811        """
 812        list_display = self.get_list_display(request)
 813        list_display_links = self.get_list_display_links(request, list_display)
 814        # Add the action checkboxes if any actions are available.
 815        if self.get_actions(request):
 816            list_display = ["action_checkbox", *list_display]
 817        sortable_by = self.get_sortable_by(request)
 818        ChangeList = self.get_changelist(request)
 819        return ChangeList(
 820            request,
 821            self.model,
 822            list_display,
 823            list_display_links,
 824            self.get_list_filter(request),
 825            self.date_hierarchy,
 826            self.get_search_fields(request),
 827            self.get_list_select_related(request),
 828            self.list_per_page,
 829            self.list_max_show_all,
 830            self.list_editable,
 831            self,
 832            sortable_by,
 833        )
 834
 835    def get_object(self, request, object_id, from_field=None):
 836        """
 837        Return an instance matching the field and value provided, the primary
 838        key is used if no field is provided. Return ``None`` if no match is
 839        found or the object_id fails validation.
 840        """
 841        queryset = self.get_queryset(request)
 842        model = queryset.model
 843        field = (
 844            model._meta.pk if from_field is None else model._meta.get_field(from_field)
 845        )
 846        try:
 847            object_id = field.to_python(object_id)
 848            return queryset.get(**{field.name: object_id})
 849        except (model.DoesNotExist, ValidationError, ValueError):
 850            return None
 851
 852    def get_changelist_form(self, request, **kwargs):
 853        """
 854        Return a Form class for use in the Formset on the changelist page.
 855        """
 856        defaults = {
 857            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
 858            **kwargs,
 859        }
 860        if defaults.get("fields") is None and not modelform_defines_fields(
 861            defaults.get("form")
 862        ):
 863            defaults["fields"] = forms.ALL_FIELDS
 864
 865        return modelform_factory(self.model, **defaults)
 866
 867    def get_changelist_formset(self, request, **kwargs):
 868        """
 869        Return a FormSet class for use on the changelist page if list_editable
 870        is used.
 871        """
 872        defaults = {
 873            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
 874            **kwargs,
 875        }
 876        return modelformset_factory(
 877            self.model,
 878            self.get_changelist_form(request),
 879            extra=0,
 880            fields=self.list_editable,
 881            **defaults
 882        )
 883
 884    def get_formsets_with_inlines(self, request, obj=None):
 885        """
 886        Yield formsets and the corresponding inlines.
 887        """
 888        for inline in self.get_inline_instances(request, obj):
 889            yield inline.get_formset(request, obj), inline
 890
 891    def get_paginator(
 892        self, request, queryset, per_page, orphans=0, allow_empty_first_page=True
 893    ):
 894        return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
 895
 896    def log_addition(self, request, object, message):
 897        """
 898        Log that an object has been successfully added.
 899
 900        The default implementation creates an admin LogEntry object.
 901        """
 902        from django.contrib.admin.models import LogEntry, ADDITION
 903
 904        return LogEntry.objects.log_action(
 905            user_id=request.user.pk,
 906            content_type_id=get_content_type_for_model(object).pk,
 907            object_id=object.pk,
 908            object_repr=str(object),
 909            action_flag=ADDITION,
 910            change_message=message,
 911        )
 912
 913    def log_change(self, request, object, message):
 914        """
 915        Log that an object has been successfully changed.
 916
 917        The default implementation creates an admin LogEntry object.
 918        """
 919        from django.contrib.admin.models import LogEntry, CHANGE
 920
 921        return LogEntry.objects.log_action(
 922            user_id=request.user.pk,
 923            content_type_id=get_content_type_for_model(object).pk,
 924            object_id=object.pk,
 925            object_repr=str(object),
 926            action_flag=CHANGE,
 927            change_message=message,
 928        )
 929
 930    def log_deletion(self, request, object, object_repr):
 931        """
 932        Log that an object will be deleted. Note that this method must be
 933        called before the deletion.
 934
 935        The default implementation creates an admin LogEntry object.
 936        """
 937        from django.contrib.admin.models import LogEntry, DELETION
 938
 939        return LogEntry.objects.log_action(
 940            user_id=request.user.pk,
 941            content_type_id=get_content_type_for_model(object).pk,
 942            object_id=object.pk,
 943            object_repr=object_repr,
 944            action_flag=DELETION,
 945        )
 946
 947    def action_checkbox(self, obj):
 948        """
 949        A list_display column containing a checkbox widget.
 950        """
 951        return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
 952
 953    action_checkbox.short_description = mark_safe(
 954        '<input type="checkbox" id="action-toggle">'
 955    )
 956
 957    def _get_base_actions(self):
 958        """Return the list of actions, prior to any request-based filtering."""
 959        actions = []
 960
 961        # Gather actions from the admin site first
 962        for (name, func) in self.admin_site.actions:
 963            description = getattr(func, "short_description", name.replace("_", " "))
 964            actions.append((func, name, description))
 965        # Add actions from this ModelAdmin.
 966        actions.extend(self.get_action(action) for action in self.actions or [])
 967        # get_action might have returned None, so filter any of those out.
 968        return filter(None, actions)
 969
 970    def _filter_actions_by_permissions(self, request, actions):
 971        """Filter out any actions that the user doesn't have access to."""
 972        filtered_actions = []
 973        for action in actions:
 974            callable = action[0]
 975            if not hasattr(callable, "allowed_permissions"):
 976                filtered_actions.append(action)
 977                continue
 978            permission_checks = (
 979                getattr(self, "has_%s_permission" % permission)
 980                for permission in callable.allowed_permissions
 981            )
 982            if any(has_permission(request) for has_permission in permission_checks):
 983                filtered_actions.append(action)
 984        return filtered_actions
 985
 986    def get_actions(self, request):
 987        """
 988        Return a dictionary mapping the names of all actions for this
 989        ModelAdmin to a tuple of (callable, name, description) for each action.
 990        """
 991        # If self.actions is set to None that means actions are disabled on
 992        # this page.
 993        if self.actions is None or IS_POPUP_VAR in request.GET:
 994            return {}
 995        actions = self._filter_actions_by_permissions(request, self._get_base_actions())
 996        return {name: (func, name, desc) for func, name, desc in actions}
 997
 998    def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
 999        """
1000        Return a list of choices for use in a form object.  Each choice is a
1001        tuple (name, description).
1002        """
1003        choices = [] + default_choices
1004        for func, name, description in self.get_actions(request).values():
1005            choice = (name, description % model_format_dict(self.opts))
1006            choices.append(choice)
1007        return choices
1008
1009    def get_action(self, action):
1010        """
1011        Return a given action from a parameter, which can either be a callable,
1012        or the name of a method on the ModelAdmin.  Return is a tuple of
1013        (callable, name, description).
1014        """
1015        # If the action is a callable, just use it.
1016        if callable(action):
1017            func = action
1018            action = action.__name__
1019
1020        # Next, look for a method. Grab it off self.__class__ to get an unbound
1021        # method instead of a bound one; this ensures that the calling
1022        # conventions are the same for functions and methods.
1023        elif hasattr(self.__class__, action):
1024            func = getattr(self.__class__, action)
1025
1026        # Finally, look for a named method on the admin site
1027        else:
1028            try:
1029                func = self.admin_site.get_action(action)
1030            except KeyError:
1031                return None
1032
1033        if hasattr(func, "short_description"):
1034            description = func.short_description
1035        else:
1036            description = capfirst(action.replace("_", " "))
1037        return func, action, description
1038
1039    def get_list_display(self, request):
1040        """
1041        Return a sequence containing the fields to be displayed on the
1042        changelist.
1043        """
1044        return self.list_display
1045
1046    def get_list_display_links(self, request, list_display):
1047        """
1048        Return a sequence containing the fields to be displayed as links
1049        on the changelist. The list_display parameter is the list of fields
1050        returned by get_list_display().
1051        """
1052        if (
1053            self.list_display_links
1054            or self.list_display_links is None
1055            or not list_display
1056        ):
1057            return self.list_display_links
1058        else:
1059            # Use only the first item in list_display as link
1060            return list(list_display)[:1]
1061
1062    def get_list_filter(self, request):
1063        """
1064        Return a sequence containing the fields to be displayed as filters in
1065        the right sidebar of the changelist page.
1066        """
1067        return self.list_filter
1068
1069    def get_list_select_related(self, request):
1070        """
1071        Return a list of fields to add to the select_related() part of the
1072        changelist items query.
1073        """
1074        return self.list_select_related
1075
1076    def get_search_fields(self, request):
1077        """
1078        Return a sequence containing the fields to be searched whenever
1079        somebody submits a search query.
1080        """
1081        return self.search_fields
1082
1083    def get_search_results(self, request, queryset, search_term):
1084        """
1085        Return a tuple containing a queryset to implement the search
1086        and a boolean indicating if the results may contain duplicates.
1087        """
1088        # Apply keyword searches.
1089        def construct_search(field_name):
1090            if field_name.startswith("^"):
1091                return "%s__istartswith" % field_name[1:]
1092            elif field_name.startswith("="):
1093                return "%s__iexact" % field_name[1:]
1094            elif field_name.startswith("@"):
1095                return "%s__search" % field_name[1:]
1096            # Use field_name if it includes a lookup.
1097            opts = queryset.model._meta
1098            lookup_fields = field_name.split(LOOKUP_SEP)
1099            # Go through the fields, following all relations.
1100            prev_field = None
1101            for path_part in lookup_fields:
1102                if path_part == "pk":
1103                    path_part = opts.pk.name
1104                try:
1105                    field = opts.get_field(path_part)
1106                except FieldDoesNotExist:
1107                    # Use valid query lookups.
1108                    if prev_field and prev_field.get_lookup(path_part):
1109                        return field_name
1110                else:
1111                    prev_field = field
1112                    if hasattr(field, "get_path_info"):
1113                        # Update opts to follow the relation.
1114                        opts = field.get_path_info()[-1].to_opts
1115            # Otherwise, use the field with icontains.
1116            return "%s__icontains" % field_name
1117
1118        use_distinct = False
1119        search_fields = self.get_search_fields(request)
1120        if search_fields and search_term:
1121            orm_lookups = [
1122                construct_search(str(search_field)) for search_field in search_fields
1123            ]
1124            for bit in search_term.split():
1125                or_queries = [
1126                    models.Q(**{orm_lookup: bit}) for orm_lookup in orm_lookups
1127                ]
1128                queryset = queryset.filter(reduce(operator.or_, or_queries))
1129            use_distinct |= any(
1130                lookup_needs_distinct(self.opts, search_spec)
1131                for search_spec in orm_lookups
1132            )
1133
1134        return queryset, use_distinct
1135
1136    def get_preserved_filters(self, request):
1137        """
1138        Return the preserved filters querystring.
1139        """
1140        match = request.resolver_match
1141        if self.preserve_filters and match:
1142            opts = self.model._meta
1143            current_url = "%s:%s" % (match.app_name, match.url_name)
1144            changelist_url = "admin:%s_%s_changelist" % (
1145                opts.app_label,
1146                opts.model_name,
1147            )
1148            if current_url == changelist_url:
1149                preserved_filters = request.GET.urlencode()
1150            else:
1151                preserved_filters = request.GET.get("_changelist_filters")
1152
1153            if preserved_filters:
1154                return urlencode({"_changelist_filters": preserved_filters})
1155        return ""
1156
1157    def construct_change_message(self, request, form, formsets, add=False):
1158        """
1159        Construct a JSON structure describing changes from a changed object.
1160        """
1161        return construct_change_message(form, formsets, add)
1162
1163    def message_user(
1164        self, request, message, level=messages.INFO, extra_tags="", fail_silently=False
1165    ):
1166        """
1167        Send a message to the user. The default implementation
1168        posts a message using the django.contrib.messages backend.
1169
1170        Exposes almost the same API as messages.add_message(), but accepts the
1171        positional arguments in a different order to maintain backwards
1172        compatibility. For convenience, it accepts the `level` argument as
1173        a string rather than the usual level number.
1174        """
1175        if not isinstance(level, int):
1176            # attempt to get the level if passed a string
1177            try:
1178                level = getattr(messages.constants, level.upper())
1179            except AttributeError:
1180                levels = messages.constants.DEFAULT_TAGS.values()
1181                levels_repr = ", ".join("`%s`" % l for l in levels)
1182                raise ValueError(
1183                    "Bad message level string: `%s`. Possible values are: %s"
1184                    % (level, levels_repr)
1185                )
1186
1187        messages.add_message(
1188            request, level, message, extra_tags=extra_tags, fail_silently=fail_silently
1189        )
1190
1191    def save_form(self, request, form, change):
1192        """
1193        Given a ModelForm return an unsaved instance. ``change`` is True if
1194        the object is being changed, and False if it's being added.
1195        """
1196        return form.save(commit=False)
1197
1198    def save_model(self, request, obj, form, change):
1199        """
1200        Given a model instance save it to the database.
1201        """
1202        obj.save()
1203
1204    def delete_model(self, request, obj):
1205        """
1206        Given a model instance delete it from the database.
1207        """
1208        obj.delete()
1209
1210    def delete_queryset(self, request, queryset):
1211        """Given a queryset, delete it from the database."""
1212        queryset.delete()
1213
1214    def save_formset(self, request, form, formset, change):
1215        """
1216        Given an inline formset save it to the database.
1217        """
1218        formset.save()
1219
1220    def save_related(self, request, form, formsets, change):
1221        """
1222        Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
1223        list of inline formsets and a boolean value based on whether the
1224        parent is being added or changed, save the related objects to the
1225        database. Note that at this point save_form() and save_model() have
1226        already been called.
1227        """
1228        form.save_m2m()
1229        for formset in formsets:
1230            self.save_formset(request, form, formset, change=change)
1231
1232    def render_change_form(
1233        self, request, context, add=False, change=False, form_url="", obj=None
1234    ):
1235        opts = self.model._meta
1236        app_label = opts.app_label
1237        preserved_filters = self.get_preserved_filters(request)
1238        form_url = add_preserved_filters(
1239            {"preserved_filters": preserved_filters, "opts": opts}, form_url
1240        )
1241        view_on_site_url = self.get_view_on_site_url(obj)
1242        has_editable_inline_admin_formsets = False
1243        for inline in context["inline_admin_formsets"]:
1244            if (
1245                inline.has_add_permission
1246                or inline.has_change_permission
1247                or inline.has_delete_permission
1248            ):
1249                has_editable_inline_admin_formsets = True
1250                break
1251        context.update(
1252            {
1253                "add": add,
1254                "change": change,
1255                "has_view_permission": self.has_view_permission(request, obj),
1256                "has_add_permission": self.has_add_permission(request),
1257                "has_change_permission": self.has_change_permission(request, obj),
1258                "has_delete_permission": self.has_delete_permission(request, obj),
1259                "has_editable_inline_admin_formsets": has_editable_inline_admin_formsets,
1260                "has_file_field": context["adminform"].form.is_multipart()
1261                or any(
1262                    admin_formset.formset.is_multipart()
1263                    for admin_formset in context["inline_admin_formsets"]
1264                ),
1265                "has_absolute_url": view_on_site_url is not None,
1266                "absolute_url": view_on_site_url,
1267                "form_url": form_url,
1268                "opts": opts,
1269                "content_type_id": get_content_type_for_model(self.model).pk,
1270                "save_as": self.save_as,
1271                "save_on_top": self.save_on_top,
1272                "to_field_var": TO_FIELD_VAR,
1273                "is_popup_var": IS_POPUP_VAR,
1274                "app_label": app_label,
1275            }
1276        )
1277        if add and self.add_form_template is not None:
1278            form_template = self.add_form_template
1279        else:
1280            form_template = self.change_form_template
1281
1282        request.current_app = self.admin_site.name
1283
1284        return TemplateResponse(
1285            request,
1286            form_template
1287            or [
1288                "admin/%s/%s/change_form.html" % (app_label, opts.model_name),
1289                "admin/%s/change_form.html" % app_label,
1290                "admin/change_form.html",
1291            ],
1292            context,
1293        )
1294
1295    def response_add(self, request, obj, post_url_continue=None):
1296        """
1297        Determine the HttpResponse for the add_view stage.
1298        """
1299        opts = obj._meta
1300        preserved_filters = self.get_preserved_filters(request)
1301        obj_url = reverse(
1302            "admin:%s_%s_change" % (opts.app_label, opts.model_name),
1303            args=(quote(obj.pk),),
1304            current_app=self.admin_site.name,
1305        )
1306        # Add a link to the object's change form if the user can edit the obj.
1307        if self.has_change_permission(request, obj):
1308            obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
1309        else:
1310            obj_repr = str(obj)
1311        msg_dict = {
1312            "name": opts.verbose_name,
1313            "obj": obj_repr,
1314        }
1315        # Here, we distinguish between different save types by checking for
1316        # the presence of keys in request.POST.
1317
1318        if IS_POPUP_VAR in request.POST:
1319            to_field = request.POST.get(TO_FIELD_VAR)
1320            if to_field:
1321                attr = str(to_field)
1322            else:
1323                attr = obj._meta.pk.attname
1324            value = obj.serializable_value(attr)
1325            popup_response_data = json.dumps({"value": str(value), "obj": str(obj),})
1326            return TemplateResponse(
1327                request,
1328                self.popup_response_template
1329                or [
1330                    "admin/%s/%s/popup_response.html"
1331                    % (opts.app_label, opts.model_name),
1332                    "admin/%s/popup_response.html" % opts.app_label,
1333                    "admin/popup_response.html",
1334                ],
1335                {"popup_response_data": popup_response_data,},
1336            )
1337
1338        elif "_continue" in request.POST or (
1339            # Redirecting after "Save as new".
1340            "_saveasnew" in request.POST
1341            and self.save_as_continue
1342            and self.has_change_permission(request, obj)
1343        ):
1344            msg = _("The {name}{obj}” was added successfully.")
1345            if self.has_change_permission(request, obj):
1346                msg += " " + _("You may edit it again below.")
1347            self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
1348            if post_url_continue is None:
1349                post_url_continue = obj_url
1350            post_url_continue = add_preserved_filters(
1351                {"preserved_filters": preserved_filters, "opts": opts},
1352                post_url_continue,
1353            )
1354            return HttpResponseRedirect(post_url_continue)
1355
1356        elif "_addanother" in request.POST:
1357            msg = format_html(
1358                _(
1359                    "The {name}{obj}” was added successfully. You may add another {name} below."
1360                ),
1361                **msg_dict
1362            )
1363            self.message_user(request, msg, messages.SUCCESS)
1364            redirect_url = request.path
1365            redirect_url = add_preserved_filters(
1366                {"preserved_filters": preserved_filters, "opts": opts}, redirect_url
1367            )
1368            return HttpResponseRedirect(redirect_url)
1369
1370        else:
1371            msg = format_html(
1372                _("The {name}{obj}” was added successfully."), **msg_dict
1373            )
1374            self.message_user(request, msg, messages.SUCCESS)
1375            return self.response_post_save_add(request, obj)
1376
1377    def response_change(self, request, obj):
1378        """
1379        Determine the HttpResponse for the change_view stage.
1380        """
1381
1382        if IS_POPUP_VAR in request.POST:
1383            opts = obj._meta
1384            to_field = request.POST.get(TO_FIELD_VAR)
1385            attr = str(to_field) if to_field else opts.pk.attname
1386            value = request.resolver_match.kwargs["object_id"]
1387            new_value = obj.serializable_value(attr)
1388            popup_response_data = json.dumps(
1389                {
1390                    "action": "change",
1391                    "value": str(value),
1392                    "obj": str(obj),
1393                    "new_value": str(new_value),
1394                }
1395            )
1396            return TemplateResponse(
1397                request,
1398                self.popup_response_template
1399                or [
1400                    "admin/%s/%s/popup_response.html"
1401                    % (opts.app_label, opts.model_name),
1402                    "admin/%s/popup_response.html" % opts.app_label,
1403                    "admin/popup_response.html",
1404                ],
1405                {"popup_response_data": popup_response_data,},
1406            )
1407
1408        opts = self.model._meta
1409        preserved_filters = self.get_preserved_filters(request)
1410
1411        msg_dict = {
1412            "name": opts.verbose_name,
1413            "obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
1414        }
1415        if "_continue" in request.POST:
1416            msg = format_html(
1417                _(
1418                    "The {name}{obj}” was changed successfully. You may edit it again below."
1419                ),
1420                **msg_dict
1421            )
1422            self.message_user(request, msg, messages.SUCCESS)
1423            redirect_url = request.path
1424            redirect_url = add_preserved_filters(
1425                {"preserved_filters": preserved_filters, "opts": opts}, redirect_url
1426            )
1427            return HttpResponseRedirect(redirect_url)
1428
1429        elif "_saveasnew" in request.POST:
1430            msg = format_html(
1431                _(
1432                    "The {name}{obj}” was added successfully. You may edit it again below."
1433                ),
1434                **msg_dict
1435            )
1436            self.message_user(request, msg, messages.SUCCESS)
1437            redirect_url = reverse(
1438                "admin:%s_%s_change" % (opts.app_label, opts.model_name),
1439                args=(obj.pk,),
1440                current_app=self.admin_site.name,
1441            )
1442            redirect_url = add_preserved_filters(
1443                {"preserved_filters": preserved_filters, "opts": opts}, redirect_url
1444            )
1445            return HttpResponseRedirect(redirect_url)
1446
1447        elif "_addanother" in request.POST:
1448            msg = format_html(
1449                _(
1450                    "The {name}{obj}” was changed successfully. You may add another {name} below."
1451                ),
1452                **msg_dict
1453            )
1454            self.message_user(request, msg, messages.SUCCESS)
1455            redirect_url = reverse(
1456                "admin:%s_%s_add" % (opts.app_label, opts.model_name),
1457                current_app=self.admin_site.name,
1458            )
1459            redirect_url = add_preserved_filters(
1460                {"preserved_filters": preserved_filters, "opts": opts}, redirect_url
1461            )
1462            return HttpResponseRedirect(redirect_url)
1463
1464        else:
1465            msg = format_html(
1466                _("The {name}{obj}” was changed successfully."), **msg_dict
1467            )
1468            self.message_user(request, msg, messages.SUCCESS)
1469            return self.response_post_save_change(request, obj)
1470
1471    def _response_post_save(self, request, obj):
1472        opts = self.model._meta
1473        if self.has_view_or_change_permission(request):
1474            post_url = reverse(
1475                "admin:%s_%s_changelist" % (opts.app_label, opts.model_name),
1476                current_app=self.admin_site.name,
1477            )
1478            preserved_filters = self.get_preserved_filters(request)
1479            post_url = add_preserved_filters(
1480                {"preserved_filters": preserved_filters, "opts": opts}, post_url
1481            )
1482        else:
1483            post_url = reverse("admin:index", current_app=self.admin_site.name)
1484        return HttpResponseRedirect(post_url)
1485
1486    def response_post_save_add(self, request, obj):
1487        """
1488        Figure out where to redirect after the 'Save' button has been pressed
1489        when adding a new object.
1490        """
1491        return self._response_post_save(request, obj)
1492
1493    def response_post_save_change(self, request, obj):
1494        """
1495        Figure out where to redirect after the 'Save' button has been pressed
1496        when editing an existing object.
1497        """
1498        return self._response_post_save(request, obj)
1499
1500    def response_action(self, request, queryset):
1501        """
1502        Handle an admin action. This is called if a request is POSTed to the
1503        changelist; it returns an HttpResponse if the action was handled, and
1504        None otherwise.
1505        """
1506
1507        # There can be multiple action forms on the page (at the top
1508        # and bottom of the change list, for example). Get the action
1509        # whose button was pushed.
1510        try:
1511            action_index = int(request.POST.get("index", 0))
1512        except ValueError:
1513            action_index = 0
1514
1515        # Construct the action form.
1516        data = request.POST.copy()
1517        data.pop(helpers.ACTION_CHECKBOX_NAME, None)
1518        data.pop("index", None)
1519
1520        # Use the action whose button was pushed
1521        try:
1522            data.update({"action": data.getlist("action")[action_index]})
1523        except IndexError:
1524            # If we didn't get an action from the chosen form that's invalid
1525            # POST data, so by deleting action it'll fail the validation check
1526            # below. So no need to do anything here
1527            pass
1528
1529        action_form = self.action_form(data, auto_id=None)
1530        action_form.fields["action"].choices = self.get_action_choices(request)
1531
1532        # If the form's valid we can handle the action.
1533        if action_form.is_valid():
1534            action = action_form.cleaned_data["action"]
1535            select_across = action_form.cleaned_data["select_across"]
1536            func = self.get_actions(request)[action][0]
1537
1538            # Get the list of selected PKs. If nothing's selected, we can't
1539            # perform an action on it, so bail. Except we want to perform
1540            # the action explicitly on all objects.
1541            selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1542            if not selected and not select_across:
1543                # Reminder that something needs to be selected or nothing will happen
1544                msg = _(
1545                    "Items must be selected in order to perform "
1546                    "actions on them. No items have been changed."
1547                )
1548                self.message_user(request, msg, messages.WARNING)
1549                return None
1550
1551            if not select_across:
1552                # Perform the action only on the selected objects
1553                queryset = queryset.filter(pk__in=selected)
1554
1555            response = func(self, request, queryset)
1556
1557            # Actions may return an HttpResponse-like object, which will be
1558            # used as the response from the POST. If not, we'll be a good
1559            # little HTTP citizen and redirect back to the changelist page.
1560            if isinstance(response, HttpResponseBase):
1561                return response
1562            else:
1563                return HttpResponseRedirect(request.get_full_path())
1564        else:
1565            msg = _("No action selected.")
1566            self.message_user(request, msg, messages.WARNING)
1567            return None
1568
1569    def response_delete(self, request, obj_display, obj_id):
1570        """
1571        Determine the HttpResponse for the delete_view stage.
1572        """
1573        opts = self.model._meta
1574
1575        if IS_POPUP_VAR in request.POST:
1576            popup_response_data = json.dumps(
1577                {"action": "delete", "value": str(obj_id),}
1578            )
1579            return TemplateResponse(
1580                request,
1581                self.popup_response_template
1582                or [
1583                    "admin/%s/%s/popup_response.html"
1584                    % (opts.app_label, opts.model_name),
1585                    "admin/%s/popup_response.html" % opts.app_label,
1586                    "admin/popup_response.html",
1587                ],
1588                {"popup_response_data": popup_response_data,},
1589            )
1590
1591        self.message_user(
1592            request,
1593            _("The %(name)s%(obj)s” was deleted successfully.")
1594            % {"name": opts.verbose_name, "obj": obj_display,},
1595            messages.SUCCESS,
1596        )
1597
1598        if self.has_change_permission(request, None):
1599            post_url = reverse(
1600                "admin:%s_%s_changelist" % (opts.app_label, opts.model_name),
1601                current_app=self.admin_site.name,
1602            )
1603            preserved_filters = self.get_preserved_filters(request)
1604            post_url = add_preserved_filters(
1605                {"preserved_filters": preserved_filters, "opts": opts}, post_url
1606            )
1607        else:
1608            post_url = reverse("admin:index", current_app=self.admin_site.name)
1609        return HttpResponseRedirect(post_url)
1610
1611    def render_delete_form(self, request, context):
1612        opts = self.model._meta
1613        app_label = opts.app_label
1614
1615        request.current_app = self.admin_site.name
1616        context.update(
1617            to_field_var=TO_FIELD_VAR, is_popup_var=IS_POPUP_VAR, media=self.media,
1618        )
1619
1620        return TemplateResponse(
1621            request,
1622            self.delete_confirmation_template
1623            or [
1624                "admin/{}/{}/delete_confirmation.html".format(
1625                    app_label, opts.model_name
1626                ),
1627                "admin/{}/delete_confirmation.html".format(app_label),
1628                "admin/delete_confirmation.html",
1629            ],
1630            context,
1631        )
1632
1633    def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
1634        # Edit permissions on parent model are required for editable inlines.
1635        can_edit_parent = (
1636            self.has_change_permission(request, obj)
1637            if obj
1638            else self.has_add_permission(request)
1639        )
1640        inline_admin_formsets = []
1641        for inline, formset in zip(inline_instances, formsets):
1642            fieldsets = list(inline.get_fieldsets(request, obj))
1643            readonly = list(inline.get_readonly_fields(request, obj))
1644            if can_edit_parent:
1645                has_add_permission = inline.has_add_permission(request, obj)
1646                has_change_permission = inline.has_change_permission(request, obj)
1647                has_delete_permission = inline.has_delete_permission(request, obj)
1648            else:
1649                # Disable all edit-permissions, and overide formset settings.
1650                has_add_permission = (
1651                    has_change_permission
1652                ) = has_delete_permission = False
1653                formset.extra = formset.max_num = 0
1654            has_view_permission = inline.has_view_permission(request, obj)
1655            prepopulated = dict(inline.get_prepopulated_fields(request, obj))
1656            inline_admin_formset = helpers.InlineAdminFormSet(
1657                inline,
1658                formset,
1659                fieldsets,
1660                prepopulated,
1661                readonly,
1662                model_admin=self,
1663                has_add_permission=has_add_permission,
1664                has_change_permission=has_change_permission,
1665                has_delete_permission=has_delete_permission,
1666                has_view_permission=has_view_permission,
1667            )
1668            inline_admin_formsets.append(inline_admin_formset)
1669        return inline_admin_formsets
1670
1671    def get_changeform_initial_data(self, request):
1672        """
1673        Get the initial form data from the request's GET params.
1674        """
1675        initial = dict(request.GET.items())
1676        for k in initial:
1677            try:
1678                f = self.model._meta.get_field(k)
1679            except FieldDoesNotExist:
1680                continue
1681            # We have to special-case M2Ms as a list of comma-separated PKs.
1682            if isinstance(f, models.ManyToManyField):
1683                initial[k] = initial[k].split(",")
1684        return initial
1685
1686    def _get_obj_does_not_exist_redirect(self, request, opts, object_id):
1687        """
1688        Create a message informing the user that the object doesn't exist
1689        and return a redirect to the admin index page.
1690        """
1691        msg = _("%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?") % {
1692            "name": opts.verbose_name,
1693            "key": unquote(object_id),
1694        }
1695        self.message_user(request, msg, messages.WARNING)
1696        url = reverse("admin:index", current_app=self.admin_site.name)
1697        return HttpResponseRedirect(url)
1698
1699    @csrf_protect_m
1700    def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
1701        with transaction.atomic(using=router.db_for_write(self.model)):
1702            return self._changeform_view(request, object_id, form_url, extra_context)
1703
1704    def _changeform_view(self, request, object_id, form_url, extra_context):
1705        to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
1706        if to_field and not self.to_field_allowed(request, to_field):
1707            raise DisallowedModelAdminToField(
1708                "The field %s cannot be referenced." % to_field
1709            )
1710
1711        model = self.model
1712        opts = model._meta
1713
1714        if request.method == "POST" and "_saveasnew" in request.POST:
1715            object_id = None
1716
1717        add = object_id is None
1718
1719        if add:
1720            if not self.has_add_permission(request):
1721                raise PermissionDenied
1722            obj = None
1723
1724        else:
1725            obj = self.get_object(request, unquote(object_id), to_field)
1726
1727            if request.method == "POST":
1728                if not self.has_change_permission(request, obj):
1729                    raise PermissionDenied
1730            else:
1731                if not self.has_view_or_change_permission(request, obj):
1732                    raise PermissionDenied
1733
1734            if obj is None:
1735                return self._get_obj_does_not_exist_redirect(request, opts, object_id)
1736
1737        ModelForm = self.get_form(request, obj, change=not add)
1738        if request.method == "POST":
1739            form = ModelForm(request.POST, request.FILES, instance=obj)
1740            form_validated = form.is_valid()
1741            if form_validated:
1742                new_object = self.save_form(request, form, change=not add)
1743            else:
1744                new_object = form.instance
1745            formsets, inline_instances = self._create_formsets(
1746                request, new_object, change=not add
1747            )
1748            if all_valid(formsets) and form_validated:
1749                self.save_model(request, new_object, form, not add)
1750                self.save_related(request, form, formsets, not add)
1751                change_message = self.construct_change_message(
1752                    request, form, formsets, add
1753                )
1754                if add:
1755                    self.log_addition(request, new_object, change_message)
1756                    return self.response_add(request, new_object)
1757                else:
1758                    self.log_change(request, new_object, change_message)
1759                    return self.response_change(request, new_object)
1760            else:
1761                form_validated = False
1762        else:
1763            if add:
1764                initial = self.get_changeform_initial_data(request)
1765                form = ModelForm(initial=initial)
1766                formsets, inline_instances = self._create_formsets(
1767                    request, form.instance, change=False
1768                )
1769            else:
1770                form = ModelForm(instance=obj)
1771                formsets, inline_instances = self._create_formsets(
1772                    request, obj, change=True
1773                )
1774
1775        if not add and not self.has_change_permission(request, obj):
1776            readonly_fields = flatten_fieldsets(self.get_fieldsets(request, obj))
1777        else:
1778            readonly_fields = self.get_readonly_fields(request, obj)
1779        adminForm = helpers.AdminForm(
1780            form,
1781            list(self.get_fieldsets(request, obj)),
1782            # Clear prepopulated fields on a view-only form to avoid a crash.
1783            self.get_prepopulated_fields(request, obj)
1784            if add or self.has_change_permission(request, obj)
1785            else {},
1786            readonly_fields,
1787            model_admin=self,
1788        )
1789        media = self.media + adminForm.media
1790
1791        inline_formsets = self.get_inline_formsets(
1792            request, formsets, inline_instances, obj
1793        )
1794        for inline_formset in inline_formsets:
1795            media = media + inline_formset.media
1796
1797        if add:
1798            title = _("Add %s")
1799        elif self.has_change_permission(request, obj):
1800            title = _("Change %s")
1801        else:
1802            title = _("View %s")
1803        context = {
1804            **self.admin_site.each_context(request),
1805            "title": title % opts.verbose_name,
1806            "adminform": adminForm,
1807            "object_id": object_id,
1808            "original": obj,
1809            "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
1810            "to_field": to_field,
1811            "media": media,
1812            "inline_admin_formsets": inline_formsets,
1813            "errors": helpers.AdminErrorList(form, formsets),
1814            "preserved_filters": self.get_preserved_filters(request),
1815        }
1816
1817        # Hide the "Save" and "Save and continue" buttons if "Save as New" was
1818        # previously chosen to prevent the interface from getting confusing.
1819        if (
1820            request.method == "POST"
1821            and not form_validated
1822            and "_saveasnew" in request.POST
1823        ):
1824            context["show_save"] = False
1825            context["show_save_and_continue"] = False
1826            # Use the change template instead of the add template.
1827            add = False
1828
1829        context.update(extra_context or {})
1830
1831        return self.render_change_form(
1832            request, context, add=add, change=not add, obj=obj, form_url=form_url
1833        )
1834
1835    def autocomplete_view(self, request):
1836        return AutocompleteJsonView.as_view(model_admin=self)(request)
1837
1838    def add_view(self, request, form_url="", extra_context=None):
1839        return self.changeform_view(request, None, form_url, extra_context)
1840
1841    def change_view(self, request, object_id, form_url="", extra_context=None):
1842        return self.changeform_view(request, object_id, form_url, extra_context)
1843
1844    def _get_edited_object_pks(self, request, prefix):
1845        """Return POST data values of list_editable primary keys."""
1846        pk_pattern = re.compile(
1847            r"{}-\d+-{}$".format(re.escape(prefix), self.model._meta.pk.name)
1848        )
1849        return [value for key, value in request.POST.items() if pk_pattern.match(key)]
1850
1851    def _get_list_editable_queryset(self, request, prefix):
1852        """
1853        Based on POST data, return a queryset of the objects that were edited
1854        via list_editable.
1855        """
1856        object_pks = self._get_edited_object_pks(request, prefix)
1857        queryset = self.get_queryset(request)
1858        validate = queryset.model._meta.pk.to_python
1859        try:
1860            for pk in object_pks:
1861                validate(pk)
1862        except ValidationError:
1863            # Disable the optimization if the POST data was tampered with.
1864            return queryset
1865        return queryset.filter(pk__in=object_pks)
1866
1867    @csrf_protect_m
1868    def changelist_view(self, request, extra_context=None):
1869        """
1870        The 'change list' admin view for this model.
1871        """
1872        from django.contrib.admin.views.main import ERROR_FLAG
1873
1874        opts = self.model._meta
1875        app_label = opts.app_label
1876        if not self.has_view_or_change_permission(request):
1877            raise PermissionDenied
1878
1879        try:
1880            cl = self.get_changelist_instance(request)
1881        except IncorrectLookupParameters:
1882            # Wacky lookup parameters were given, so redirect to the main
1883            # changelist page, without parameters, and pass an 'invalid=1'
1884            # parameter via the query string. If wacky parameters were given
1885            # and the 'invalid=1' parameter was already in the query string,
1886            # something is screwed up with the database, so display an error
1887            # page.
1888            if ERROR_FLAG in request.GET:
1889                return SimpleTemplateResponse(
1890                    "admin/invalid_setup.html", {"title": _("Database error"),}
1891                )
1892            return HttpResponseRedirect(request.path + "?" + ERROR_FLAG + "=1")
1893
1894        # If the request was POSTed, this might be a bulk action or a bulk
1895        # edit. Try to look up an action or confirmation first, but if this
1896        # isn't an action the POST will fall through to the bulk edit check,
1897        # below.
1898        action_failed = False
1899        selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1900
1901        actions = self.get_actions(request)
1902        # Actions with no confirmation
1903        if (
1904            actions
1905            and request.method == "POST"
1906            and "index" in request.POST
1907            and "_save" not in request.POST
1908        ):
1909            if selected:
1910                response = self.response_action(
1911                    request, queryset=cl.get_queryset(request)
1912                )
1913                if response:
1914                    return response
1915                else:
1916                    action_failed = True
1917            else:
1918                msg = _(
1919                    "Items must be selected in order to perform "
1920                    "actions on them. No items have been changed."
1921                )
1922                self.message_user(request, msg, messages.WARNING)
1923                action_failed = True
1924
1925        # Actions with confirmation
1926        if (
1927            actions
1928            and request.method == "POST"
1929            and helpers.ACTION_CHECKBOX_NAME in request.POST
1930            and "index" not in request.POST
1931            and "_save" not in request.POST
1932        ):
1933            if selected:
1934                response = self.response_action(
1935                    request, queryset=cl.get_queryset(request)
1936                )
1937                if response:
1938                    return response
1939                else:
1940                    action_failed = True
1941
1942        if action_failed:
1943            # Redirect back to the changelist page to avoid resubmitting the
1944            # form if the user refreshes the browser or uses the "No, take
1945            # me back" button on the action confirmation page.
1946            return HttpResponseRedirect(request.get_full_path())
1947
1948        # If we're allowing changelist editing, we need to construct a formset
1949        # for the changelist given all the fields to be edited. Then we'll
1950        # use the formset to validate/process POSTed data.
1951        formset = cl.formset = None
1952
1953        # Handle POSTed bulk-edit data.
1954        if request.method == "POST" and cl.list_editable and "_save" in request.POST:
1955            if not self.has_change_permission(request):
1956                raise PermissionDenied
1957            FormSet = self.get_changelist_formset(request)
1958            modified_objects = self._get_list_editable_queryset(
1959                request, FormSet.get_default_prefix()
1960            )
1961            formset = cl.formset = FormSet(
1962                request.POST, request.FILES, queryset=modified_objects
1963            )
1964            if formset.is_valid():
1965                changecount = 0
1966                for form in formset.forms:
1967                    if form.has_changed():
1968                        obj = self.save_form(request, form, change=True)
1969                        self.save_model(request, obj, form, change=True)
1970                        self.save_related(request, form, formsets=[], change=True)
1971                        change_msg = self.construct_change_message(request, form, None)
1972                        self.log_change(request, obj, change_msg)
1973                        changecount += 1
1974
1975                if changecount:
1976                    msg = ngettext(
1977                        "%(count)s %(name)s was changed successfully.",
1978                        "%(count)s %(name)s were changed successfully.",
1979                        changecount,
1980                    ) % {
1981                        "count": changecount,
1982                        "name": model_ngettext(opts, changecount),
1983                    }
1984                    self.message_user(request, msg, messages.SUCCESS)
1985
1986                return HttpResponseRedirect(request.get_full_path())
1987
1988        # Handle GET -- construct a formset for display.
1989        elif cl.list_editable and self.has_change_permission(request):
1990            FormSet = self.get_changelist_formset(request)
1991            formset = cl.formset = FormSet(queryset=cl.result_list)
1992
1993        # Build the list of media to be used by the formset.
1994        if formset:
1995            media = self.media + formset.media
1996        else:
1997            media = self.media
1998
1999        # Build the action form and populate it with available actions.
2000        if actions:
2001            action_form = self.action_form(auto_id=None)
2002            action_form.fields["action"].choices = self.get_action_choices(request)
2003            media += action_form.media
2004        else:
2005            action_form = None
2006
2007        selection_note_all = ngettext(
2008            "%(total_count)s selected", "All %(total_count)s selected", cl.result_count
2009        )
2010
2011        context = {
2012            **self.admin_site.each_context(request),
2013            "module_name": str(opts.verbose_name_plural),
2014            "selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)},
2015            "selection_note_all": selection_note_all % {"total_count": cl.result_count},
2016            "title": cl.title,
2017            "is_popup": cl.is_popup,
2018            "to_field": cl.to_field,
2019            "cl": cl,
2020            "media": media,
2021            "has_add_permission": self.has_add_permission(request),
2022            "opts": cl.opts,
2023            "action_form": action_form,
2024            "actions_on_top": self.actions_on_top,
2025            "actions_on_bottom": self.actions_on_bottom,
2026            "actions_selection_counter": self.actions_selection_counter,
2027            "preserved_filters": self.get_preserved_filters(request),
2028            **(extra_context or {}),
2029        }
2030
2031        request.current_app = self.admin_site.name
2032
2033        return TemplateResponse(
2034            request,
2035            self.change_list_template
2036            or [
2037                "admin/%s/%s/change_list.html" % (app_label, opts.model_name),
2038                "admin/%s/change_list.html" % app_label,
2039                "admin/change_list.html",
2040            ],
2041            context,
2042        )
2043
2044    def get_deleted_objects(self, objs, request):
2045        """
2046        Hook for customizing the delete process for the delete view and the
2047        "delete selected" action.
2048        """
2049        return get_deleted_objects(objs, request, self.admin_site)
2050
2051    @csrf_protect_m
2052    def delete_view(self, request, object_id, extra_context=None):
2053        with transaction.atomic(using=router.db_for_write(self.model)):
2054            return self._delete_view(request, object_id, extra_context)
2055
2056    def _delete_view(self, request, object_id, extra_context):
2057        "The 'delete' admin view for this model."
2058        opts = self.model._meta
2059        app_label = opts.app_label
2060
2061        to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
2062        if to_field and not self.to_field_allowed(request, to_field):
2063            raise DisallowedModelAdminToField(
2064                "The field %s cannot be referenced." % to_field
2065            )
2066
2067        obj = self.get_object(request, unquote(object_id), to_field)
2068
2069        if not self.has_delete_permission(request, obj):
2070            raise PermissionDenied
2071
2072        if obj is None:
2073            return self._get_obj_does_not_exist_redirect(request, opts, object_id)
2074
2075        # Populate deleted_objects, a data structure of all related objects that
2076        # will also be deleted.
2077        (
2078            deleted_objects,
2079            model_count,
2080            perms_needed,
2081            protected,
2082        ) = self.get_deleted_objects([obj], request)
2083
2084        if request.POST and not protected:  # The user has confirmed the deletion.
2085            if perms_needed:
2086                raise PermissionDenied
2087            obj_display = str(obj)
2088            attr = str(to_field) if to_field else opts.pk.attname
2089            obj_id = obj.serializable_value(attr)
2090            self.log_deletion(request, obj, obj_display)
2091            self.delete_model(request, obj)
2092
2093            return self.response_delete(request, obj_display, obj_id)
2094
2095        object_name = str(opts.verbose_name)
2096
2097        if perms_needed or protected:
2098            title = _("Cannot delete %(name)s") % {"name": object_name}
2099        else:
2100            title = _("Are you sure?")
2101
2102        context = {
2103            **self.admin_site.each_context(request),
2104            "title": title,
2105            "object_name": object_name,
2106            "object": obj,
2107            "deleted_objects": deleted_objects,
2108            "model_count": dict(model_count).items(),
2109            "perms_lacking": perms_needed,
2110            "protected": protected,
2111            "opts": opts,
2112            "app_label": app_label,
2113            "preserved_filters": self.get_preserved_filters(request),
2114            "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
2115            "to_field": to_field,
2116            **(extra_context or {}),
2117        }
2118
2119        return self.render_delete_form(request, context)
2120
2121    def history_view(self, request, object_id, extra_context=None):
2122        "The 'history' admin view for this model."
2123        from django.contrib.admin.models import LogEntry
2124
2125        # First check if the user can see this history.
2126        model = self.model
2127        obj = self.get_object(request, unquote(object_id))
2128        if obj is None:
2129            return self._get_obj_does_not_exist_redirect(
2130                request, model._meta, object_id
2131            )
2132
2133        if not self.has_view_or_change_permission(request, obj):
2134            raise PermissionDenied
2135
2136        # Then get the history for this object.
2137        opts = model._meta
2138        app_label = opts.app_label
2139        action_list = (
2140            LogEntry.objects.filter(
2141                object_id=unquote(object_id),
2142                content_type=get_content_type_for_model(model),
2143            )
2144            .select_related()
2145            .order_by("action_time")
2146        )
2147
2148        context = {
2149            **self.admin_site.each_context(request),
2150            "title": _("Change history: %s") % obj,
2151            "action_list": action_list,
2152            "module_name": str(capfirst(opts.verbose_name_plural)),
2153            "object": obj,
2154            "opts": opts,
2155            "preserved_filters": self.get_preserved_filters(request),
2156            **(extra_context or {}),
2157        }
2158
2159        request.current_app = self.admin_site.name
2160
2161        return TemplateResponse(
2162            request,
2163            self.object_history_template
2164            or [
2165                "admin/%s/%s/object_history.html" % (app_label, opts.model_name),
2166                "admin/%s/object_history.html" % app_label,
2167                "admin/object_history.html",
2168            ],
2169            context,
2170        )
2171
2172    def _create_formsets(self, request, obj, change):
2173        "Helper function to generate formsets for add/change_view."
2174        formsets = []
2175        inline_instances = []
2176        prefixes = {}
2177        get_formsets_args = [request]
2178        if change:
2179            get_formsets_args.append(obj)
2180        for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
2181            prefix = FormSet.get_default_prefix()
2182            prefixes[prefix] = prefixes.get(prefix, 0) + 1
2183            if prefixes[prefix] != 1 or not prefix:
2184                prefix = "%s-%s" % (prefix, prefixes[prefix])
2185            formset_params = {
2186                "instance": obj,
2187                "prefix": prefix,
2188                "queryset": inline.get_queryset(request),
2189            }
2190            if request.method == "POST":
2191                formset_params.update(
2192                    {
2193                        "data": request.POST.copy(),
2194                        "files": request.FILES,
2195                        "save_as_new": "_saveasnew" in request.POST,
2196                    }
2197                )
2198            formset = FormSet(**formset_params)
2199
2200            def user_deleted_form(request, obj, formset, index):
2201                """Return whether or not the user deleted the form."""
2202                return (
2203                    inline.has_delete_permission(request, obj)
2204                    and "{}-{}-DELETE".format(formset.prefix, index) in request.POST
2205                )
2206
2207            # Bypass validation of each view-only inline form (since the form's
2208            # data won't be in request.POST), unless the form was deleted.
2209            if not inline.has_change_permission(request, obj if change else None):
2210                for index, form in enumerate(formset.initial_forms):
2211                    if user_deleted_form(request, obj, formset, index):
2212                        continue
2213                    form._errors = {}
2214                    form.cleaned_data = form.initial
2215            formsets.append(formset)
2216            inline_instances.append(inline)
2217        return formsets, inline_instances
2218
2219
2220class InlineModelAdmin(BaseModelAdmin):
2221    """
2222    Options for inline editing of ``model`` instances.
2223
2224    Provide ``fk_name`` to specify the attribute name of the ``ForeignKey``
2225    from ``model`` to its parent. This is required if ``model`` has more than
2226    one ``ForeignKey`` to its parent.
2227    """
2228
2229    model = None
2230    fk_name = None
2231    formset = BaseInlineFormSet
2232    extra = 3
2233    min_num = None
2234    max_num = None
2235    template = None
2236    verbose_name = None
2237    verbose_name_plural = None
2238    can_delete = True
2239    show_change_link = False
2240    checks_class = InlineModelAdminChecks
2241    classes = None
2242
2243    def __init__(self, parent_model, admin_site):
2244        self.admin_site = admin_site
2245        self.parent_model = parent_model
2246        self.opts = self.model._meta
2247        self.has_registered_model = admin_site.is_registered(self.model)
2248        super().__init__()
2249        if self.verbose_name is None:
2250            self.verbose_name = self.model._meta.verbose_name
2251        if self.verbose_name_plural is None:
2252            self.verbose_name_plural = self.model._meta.verbose_name_plural
2253
2254    @property
2255    def media(self):
2256        extra = "" if settings.DEBUG else ".min"
2257        js = [
2258            "vendor/jquery/jquery%s.js" % extra,
2259            "jquery.init.js",
2260            "inlines%s.js" % extra,
2261        ]
2262        if self.filter_vertical or self.filter_horizontal:
2263            js.extend(["SelectBox.js", "SelectFilter2.js"])
2264        if self.classes and "collapse" in self.classes:
2265            js.append("collapse%s.js" % extra)
2266        return forms.Media(js=["admin/js/%s" % url for url in js])
2267
2268    def get_extra(self, request, obj=None, **kwargs):
2269        """Hook for customizing the number of extra inline forms."""
2270        return self.extra
2271
2272    def get_min_num(self, request, obj=None, **kwargs):
2273        """Hook for customizing the min number of inline forms."""
2274        return self.min_num
2275
2276    def get_max_num(self, request, obj=None, **kwargs):
2277        """Hook for customizing the max number of extra inline forms."""
2278        return self.max_num
2279
2280    def get_formset(self, request, obj=None, **kwargs):
2281        """Return a BaseInlineFormSet class for use in admin add/change views."""
2282        if "fields" in kwargs:
2283            fields = kwargs.pop("fields")
2284        else:
2285            fields = flatten_fieldsets(self.get_fieldsets(request, obj))
2286        excluded = self.get_exclude(request, obj)
2287        exclude = [] if excluded is None else list(excluded)
2288        exclude.extend(self.get_readonly_fields(request, obj))
2289        if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
2290            # Take the custom ModelForm's Meta.exclude into account only if the
2291            # InlineModelAdmin doesn't define its own.
2292            exclude.extend(self.form._meta.exclude)
2293        # If exclude is an empty list we use None, since that's the actual
2294        # default.
2295        exclude = exclude or None
2296        can_delete = self.can_delete and self.has_delete_permission(request, obj)
2297        defaults = {
2298            "form": self.form,
2299            "formset": self.formset,
2300            "fk_name": self.fk_name,
2301            "fields": fields,
2302            "exclude": exclude,
2303            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
2304            "extra": self.get_extra(request, obj, **kwargs),
2305            "min_num": self.get_min_num(request, obj, **kwargs),
2306            "max_num": self.get_max_num(request, obj, **kwargs),
2307            "can_delete": can_delete,
2308            **kwargs,
2309        }
2310
2311        base_model_form = defaults["form"]
2312        can_change = self.has_change_permission(request, obj) if request else True
2313        can_add = self.has_add_permission(request, obj) if request else True
2314
2315        class DeleteProtectedModelForm(base_model_form):
2316            def hand_clean_DELETE(self):
2317                """
2318                We don't validate the 'DELETE' field itself because on
2319                templates it's not rendered using the field information, but
2320                just using a generic "deletion_field" of the InlineModelAdmin.
2321                """
2322                if self.cleaned_data.get(DELETION_FIELD_NAME, False):
2323                    using = router.db_for_write(self._meta.model)
2324                    collector = NestedObjects(using=using)
2325                    if self.instance._state.adding:
2326                        return
2327                    collector.collect([self.instance])
2328                    if collector.protected:
2329                        objs = []
2330                        for p in collector.protected:
2331                            objs.append(
2332                                # Translators: Model verbose name and instance representation,
2333                                # suitable to be an item in a list.
2334                                _("%(class_name)s %(instance)s")
2335                                % {"class_name": p._meta.verbose_name, "instance": p}
2336                            )
2337                        params = {
2338                            "class_name": self._meta.model._meta.verbose_name,
2339                            "instance": self.instance,
2340                            "related_objects": get_text_list(objs, _("and")),
2341                        }
2342                        msg = _(
2343                            "Deleting %(class_name)s %(instance)s would require "
2344                            "deleting the following protected related objects: "
2345                            "%(related_objects)s"
2346                        )
2347                        raise ValidationError(
2348                            msg, code="deleting_protected", params=params
2349                        )
2350
2351            def is_valid(self):
2352                result = super().is_valid()
2353                self.hand_clean_DELETE()
2354                return result
2355
2356            def has_changed(self):
2357                # Protect against unauthorized edits.
2358                if not can_change and not self.instance._state.adding:
2359                    return False
2360                if not can_add and self.instance._state.adding:
2361                    return False
2362                return super().has_changed()
2363
2364        defaults["form"] = DeleteProtectedModelForm
2365
2366        if defaults["fields"] is None and not modelform_defines_fields(
2367            defaults["form"]
2368        ):
2369            defaults["fields"] = forms.ALL_FIELDS
2370
2371        return inlineformset_factory(self.parent_model, self.model, **defaults)
2372
2373    def _get_form_for_get_fields(self, request, obj=None):
2374        return self.get_formset(request, obj, fields=None).form
2375
2376    def get_queryset(self, request):
2377        queryset = super().get_queryset(request)
2378        if not self.has_view_or_change_permission(request):
2379            queryset = queryset.none()
2380        return queryset
2381
2382    def _has_any_perms_for_target_model(self, request, perms):
2383        """
2384        This method is called only when the ModelAdmin's model is for an
2385        ManyToManyField's implicit through model (if self.opts.auto_created).
2386        Return True if the user has any of the given permissions ('add',
2387        'change', etc.) for the model that points to the through model.
2388        """
2389        opts = self.opts
2390        # Find the target model of an auto-created many-to-many relationship.
2391        for field in opts.fields:
2392            if field.remote_field and field.remote_field.model != self.parent_model:
2393                opts = field.remote_field.model._meta
2394                break
2395        return any(
2396            request.user.has_perm(
2397                "%s.%s" % (opts.app_label, get_permission_codename(perm, opts))
2398            )
2399            for perm in perms
2400        )
2401
2402    def has_add_permission(self, request, obj):
2403        if self.opts.auto_created:
2404            # Auto-created intermediate models don't have their own
2405            # permissions. The user needs to have the change permission for the
2406            # related model in order to be able to do anything with the
2407            # intermediate model.
2408            return self._has_any_perms_for_target_model(request, ["change"])
2409        return super().has_add_permission(request)
2410
2411    def has_change_permission(self, request, obj=None):
2412        if self.opts.auto_created:
2413            # Same comment as has_add_permission().
2414            return self._has_any_perms_for_target_model(request, ["change"])
2415        return super().has_change_permission(request)
2416
2417    def has_delete_permission(self, request, obj=None):
2418        if self.opts.auto_created:
2419            # Same comment as has_add_permission().
2420            return self._has_any_perms_for_target_model(request, ["change"])
2421        return super().has_delete_permission(request, obj)
2422
2423    def has_view_permission(self, request, obj=None):
2424        if self.opts.auto_created:
2425            # Same comment as has_add_permission(). The 'change' permission
2426            # also implies the 'view' permission.
2427            return self._has_any_perms_for_target_model(request, ["view", "change"])
2428        return super().has_view_permission(request)
2429
2430
2431class StackedInline(InlineModelAdmin):
2432    template = "admin/edit_inline/stacked.html"
2433
2434
2435class TabularInline(InlineModelAdmin):
2436    template = "admin/edit_inline/tabular.html"

django/contrib/admin/widgets.py

  1"""
  2Form Widget classes specific to the Django admin site.
  3"""
  4import copy
  5import json
  6
  7from django import forms
  8from django.conf import settings
  9from django.core.exceptions import ValidationError
 10from django.core.validators import URLValidator
 11from django.db.models.deletion import CASCADE
 12from django.urls import reverse
 13from django.urls.exceptions import NoReverseMatch
 14from django.utils.html import smart_urlquote
 15from django.utils.safestring import mark_safe
 16from django.utils.text import Truncator
 17from django.utils.translation import get_language, gettext as _
 18
 19
 20class FilteredSelectMultiple(forms.SelectMultiple):
 21    """
 22    A SelectMultiple with a JavaScript filter interface.
 23
 24    Note that the resulting JavaScript assumes that the jsi18n
 25    catalog has been loaded in the page
 26    """
 27
 28    @property
 29    def media(self):
 30        extra = "" if settings.DEBUG else ".min"
 31        js = [
 32            "vendor/jquery/jquery%s.js" % extra,
 33            "jquery.init.js",
 34            "core.js",
 35            "SelectBox.js",
 36            "SelectFilter2.js",
 37        ]
 38        return forms.Media(js=["admin/js/%s" % path for path in js])
 39
 40    def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
 41        self.verbose_name = verbose_name
 42        self.is_stacked = is_stacked
 43        super().__init__(attrs, choices)
 44
 45    def get_context(self, name, value, attrs):
 46        context = super().get_context(name, value, attrs)
 47        context["widget"]["attrs"]["class"] = "selectfilter"
 48        if self.is_stacked:
 49            context["widget"]["attrs"]["class"] += "stacked"
 50        context["widget"]["attrs"]["data-field-name"] = self.verbose_name
 51        context["widget"]["attrs"]["data-is-stacked"] = int(self.is_stacked)
 52        return context
 53
 54
 55class AdminDateWidget(forms.DateInput):
 56    class Media:
 57        js = [
 58            "admin/js/calendar.js",
 59            "admin/js/admin/DateTimeShortcuts.js",
 60        ]
 61
 62    def __init__(self, attrs=None, format=None):
 63        attrs = {"class": "vDateField", "size": "10", **(attrs or {})}
 64        super().__init__(attrs=attrs, format=format)
 65
 66
 67class AdminTimeWidget(forms.TimeInput):
 68    class Media:
 69        js = [
 70            "admin/js/calendar.js",
 71            "admin/js/admin/DateTimeShortcuts.js",
 72        ]
 73
 74    def __init__(self, attrs=None, format=None):
 75        attrs = {"class": "vTimeField", "size": "8", **(attrs or {})}
 76        super().__init__(attrs=attrs, format=format)
 77
 78
 79class AdminSplitDateTime(forms.SplitDateTimeWidget):
 80    """
 81    A SplitDateTime Widget that has some admin-specific styling.
 82    """
 83
 84    template_name = "admin/widgets/split_datetime.html"
 85
 86    def __init__(self, attrs=None):
 87        widgets = [AdminDateWidget, AdminTimeWidget]
 88        # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
 89        # we want to define widgets.
 90        forms.MultiWidget.__init__(self, widgets, attrs)
 91
 92    def get_context(self, name, value, attrs):
 93        context = super().get_context(name, value, attrs)
 94        context["date_label"] = _("Date:")
 95        context["time_label"] = _("Time:")
 96        return context
 97
 98
 99class AdminRadioSelect(forms.RadioSelect):
100    template_name = "admin/widgets/radio.html"
101
102
103class AdminFileWidget(forms.ClearableFileInput):
104    template_name = "admin/widgets/clearable_file_input.html"
105
106
107def url_params_from_lookup_dict(lookups):
108    """
109    Convert the type of lookups specified in a ForeignKey limit_choices_to
110    attribute to a dictionary of query parameters
111    """
112    params = {}
113    if lookups and hasattr(lookups, "items"):
114        for k, v in lookups.items():
115            if callable(v):
116                v = v()
117            if isinstance(v, (tuple, list)):
118                v = ",".join(str(x) for x in v)
119            elif isinstance(v, bool):
120                v = ("0", "1")[v]
121            else:
122                v = str(v)
123            params[k] = v
124    return params
125
126
127class ForeignKeyRawIdWidget(forms.TextInput):
128    """
129    A Widget for displaying ForeignKeys in the "raw_id" interface rather than
130    in a <select> box.
131    """
132
133    template_name = "admin/widgets/foreign_key_raw_id.html"
134
135    def __init__(self, rel, admin_site, attrs=None, using=None):
136        self.rel = rel
137        self.admin_site = admin_site
138        self.db = using
139        super().__init__(attrs)
140
141    def get_context(self, name, value, attrs):
142        context = super().get_context(name, value, attrs)
143        rel_to = self.rel.model
144        if rel_to in self.admin_site._registry:
145            # The related object is registered with the same AdminSite
146            related_url = reverse(
147                "admin:%s_%s_changelist"
148                % (rel_to._meta.app_label, rel_to._meta.model_name,),
149                current_app=self.admin_site.name,
150            )
151
152            params = self.url_parameters()
153            if params:
154                related_url += "?" + "&amp;".join(
155                    "%s=%s" % (k, v) for k, v in params.items()
156                )
157            context["related_url"] = mark_safe(related_url)
158            context["link_title"] = _("Lookup")
159            # The JavaScript code looks for this class.
160            context["widget"]["attrs"].setdefault("class", "vForeignKeyRawIdAdminField")
161        else:
162            context["related_url"] = None
163        if context["widget"]["value"]:
164            context["link_label"], context["link_url"] = self.label_and_url_for_value(
165                value
166            )
167        else:
168            context["link_label"] = None
169        return context
170
171    def base_url_parameters(self):
172        limit_choices_to = self.rel.limit_choices_to
173        if callable(limit_choices_to):
174            limit_choices_to = limit_choices_to()
175        return url_params_from_lookup_dict(limit_choices_to)
176
177    def url_parameters(self):
178        from django.contrib.admin.views.main import TO_FIELD_VAR
179
180        params = self.base_url_parameters()
181        params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
182        return params
183
184    def label_and_url_for_value(self, value):
185        key = self.rel.get_related_field().name
186        try:
187            obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
188        except (ValueError, self.rel.model.DoesNotExist, ValidationError):
189            return "", ""
190
191        try:
192            url = reverse(
193                "%s:%s_%s_change"
194                % (
195                    self.admin_site.name,
196                    obj._meta.app_label,
197                    obj._meta.object_name.lower(),
198                ),
199                args=(obj.pk,),
200            )
201        except NoReverseMatch:
202            url = ""  # Admin not registered for target model.
203
204        return Truncator(obj).words(14), url
205
206
207class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
208    """
209    A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
210    in a <select multiple> box.
211    """
212
213    template_name = "admin/widgets/many_to_many_raw_id.html"
214
215    def get_context(self, name, value, attrs):
216        context = super().get_context(name, value, attrs)
217        if self.rel.model in self.admin_site._registry:
218            # The related object is registered with the same AdminSite
219            context["widget"]["attrs"]["class"] = "vManyToManyRawIdAdminField"
220        return context
221
222    def url_parameters(self):
223        return self.base_url_parameters()
224
225    def label_and_url_for_value(self, value):
226        return "", ""
227
228    def value_from_datadict(self, data, files, name):
229        value = data.get(name)
230        if value:
231            return value.split(",")
232
233    def format_value(self, value):
234        return ",".join(str(v) for v in value) if value else ""
235
236
237class RelatedFieldWidgetWrapper(forms.Widget):
238    """
239    This class is a wrapper to a given widget to add the add icon for the
240    admin interface.
241    """
242
243    template_name = "admin/widgets/related_widget_wrapper.html"
244
245    def __init__(
246        self,
247        widget,
248        rel,
249        admin_site,
250        can_add_related=None,
251        can_change_related=False,
252        can_delete_related=False,
253        can_view_related=False,
254    ):
255        self.needs_multipart_form = widget.needs_multipart_form
256        self.attrs = widget.attrs
257        self.choices = widget.choices
258        self.widget = widget
259        self.rel = rel
260        # Backwards compatible check for whether a user can add related
261        # objects.
262        if can_add_related is None:
263            can_add_related = rel.model in admin_site._registry
264        self.can_add_related = can_add_related
265        # XXX: The UX does not support multiple selected values.
266        multiple = getattr(widget, "allow_multiple_selected", False)
267        self.can_change_related = not multiple and can_change_related
268        # XXX: The deletion UX can be confusing when dealing with cascading deletion.
269        cascade = getattr(rel, "on_delete", None) is CASCADE
270        self.can_delete_related = not multiple and not cascade and can_delete_related
271        self.can_view_related = not multiple and can_view_related
272        # so we can check if the related object is registered with this AdminSite
273        self.admin_site = admin_site
274
275    def __deepcopy__(self, memo):
276        obj = copy.copy(self)
277        obj.widget = copy.deepcopy(self.widget, memo)
278        obj.attrs = self.widget.attrs
279        memo[id(self)] = obj
280        return obj
281
282    @property
283    def is_hidden(self):
284        return self.widget.is_hidden
285
286    @property
287    def media(self):
288        return self.widget.media
289
290    def get_related_url(self, info, action, *args):
291        return reverse(
292            "admin:%s_%s_%s" % (info + (action,)),
293            current_app=self.admin_site.name,
294            args=args,
295        )
296
297    def get_context(self, name, value, attrs):
298        from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
299
300        rel_opts = self.rel.model._meta
301        info = (rel_opts.app_label, rel_opts.model_name)
302        self.widget.choices = self.choices
303        url_params = "&".join(
304            "%s=%s" % param
305            for param in [
306                (TO_FIELD_VAR, self.rel.get_related_field().name),
307                (IS_POPUP_VAR, 1),
308            ]
309        )
310        context = {
311            "rendered_widget": self.widget.render(name, value, attrs),
312            "is_hidden": self.is_hidden,
313            "name": name,
314            "url_params": url_params,
315            "model": rel_opts.verbose_name,
316            "can_add_related": self.can_add_related,
317            "can_change_related": self.can_change_related,
318            "can_delete_related": self.can_delete_related,
319            "can_view_related": self.can_view_related,
320        }
321        if self.can_add_related:
322            context["add_related_url"] = self.get_related_url(info, "add")
323        if self.can_delete_related:
324            context["delete_related_template_url"] = self.get_related_url(
325                info, "delete", "__fk__"
326            )
327        if self.can_view_related or self.can_change_related:
328            context["change_related_template_url"] = self.get_related_url(
329                info, "change", "__fk__"
330            )
331        return context
332
333    def value_from_datadict(self, data, files, name):
334        return self.widget.value_from_datadict(data, files, name)
335
336    def value_omitted_from_data(self, data, files, name):
337        return self.widget.value_omitted_from_data(data, files, name)
338
339    def id_for_label(self, id_):
340        return self.widget.id_for_label(id_)
341
342
343class AdminTextareaWidget(forms.Textarea):
344    def __init__(self, attrs=None):
345        super().__init__(attrs={"class": "vLargeTextField", **(attrs or {})})
346
347
348class AdminTextInputWidget(forms.TextInput):
349    def __init__(self, attrs=None):
350        super().__init__(attrs={"class": "vTextField", **(attrs or {})})
351
352
353class AdminEmailInputWidget(forms.EmailInput):
354    def __init__(self, attrs=None):
355        super().__init__(attrs={"class": "vTextField", **(attrs or {})})
356
357
358class AdminURLFieldWidget(forms.URLInput):
359    template_name = "admin/widgets/url.html"
360
361    def __init__(self, attrs=None, validator_class=URLValidator):
362        super().__init__(attrs={"class": "vURLField", **(attrs or {})})
363        self.validator = validator_class()
364
365    def get_context(self, name, value, attrs):
366        try:
367            self.validator(value if value else "")
368            url_valid = True
369        except ValidationError:
370            url_valid = False
371        context = super().get_context(name, value, attrs)
372        context["current_label"] = _("Currently:")
373        context["change_label"] = _("Change:")
374        context["widget"]["href"] = (
375            smart_urlquote(context["widget"]["value"]) if value else ""
376        )
377        context["url_valid"] = url_valid
378        return context
379
380
381class AdminIntegerFieldWidget(forms.NumberInput):
382    class_name = "vIntegerField"
383
384    def __init__(self, attrs=None):
385        super().__init__(attrs={"class": self.class_name, **(attrs or {})})
386
387
388class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget):
389    class_name = "vBigIntegerField"
390
391
392class AdminUUIDInputWidget(forms.TextInput):
393    def __init__(self, attrs=None):
394        super().__init__(attrs={"class": "vUUIDField", **(attrs or {})})
395
396
397# Mapping of lowercase language codes [returned by Django's get_language()] to
398# language codes supported by select2.
399# See django/contrib/admin/static/admin/js/vendor/select2/i18n/*
400SELECT2_TRANSLATIONS = {
401    x.lower(): x
402    for x in [
403        "ar",
404        "az",
405        "bg",
406        "ca",
407        "cs",
408        "da",
409        "de",
410        "el",
411        "en",
412        "es",
413        "et",
414        "eu",
415        "fa",
416        "fi",
417        "fr",
418        "gl",
419        "he",
420        "hi",
421        "hr",
422        "hu",
423        "id",
424        "is",
425        "it",
426        "ja",
427        "km",
428        "ko",
429        "lt",
430        "lv",
431        "mk",
432        "ms",
433        "nb",
434        "nl",
435        "pl",
436        "pt-BR",
437        "pt",
438        "ro",
439        "ru",
440        "sk",
441        "sr-Cyrl",
442        "sr",
443        "sv",
444        "th",
445        "tr",
446        "uk",
447        "vi",
448    ]
449}
450SELECT2_TRANSLATIONS.update({"zh-hans": "zh-CN", "zh-hant": "zh-TW"})
451
452
453class AutocompleteMixin:
454    """
455    Select widget mixin that loads options from AutocompleteJsonView via AJAX.
456
457    Renders the necessary data attributes for select2 and adds the static form
458    media.
459    """
460
461    url_name = "%s:%s_%s_autocomplete"
462
463    def __init__(self, rel, admin_site, attrs=None, choices=(), using=None):
464        self.rel = rel
465        self.admin_site = admin_site
466        self.db = using
467        self.choices = choices
468        self.attrs = {} if attrs is None else attrs.copy()
469
470    def get_url(self):
471        model = self.rel.model
472        return reverse(
473            self.url_name
474            % (self.admin_site.name, model._meta.app_label, model._meta.model_name)
475        )
476
477    def build_attrs(self, base_attrs, extra_attrs=None):
478        """
479        Set select2's AJAX attributes.
480
481        Attributes can be set using the html5 data attribute.
482        Nested attributes require a double dash as per
483        https://select2.org/configuration/data-attributes#nested-subkey-options
484        """
485        attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
486        attrs.setdefault("class", "")
487        attrs.update(
488            {
489                "data-ajax--cache": "true",
490                "data-ajax--delay": 250,
491                "data-ajax--type": "GET",
492                "data-ajax--url": self.get_url(),
493                "data-theme": "admin-autocomplete",
494                "data-allow-clear": json.dumps(not self.is_required),
495                "data-placeholder": "",  # Allows clearing of the input.
496                "class": attrs["class"]
497                + (" " if attrs["class"] else "")
498                + "admin-autocomplete",
499            }
500        )
501        return attrs
502
503    def optgroups(self, name, value, attr=None):
504        """Return selected options based on the ModelChoiceIterator."""
505        default = (None, [], 0)
506        groups = [default]
507        has_selected = False
508        selected_choices = {
509            str(v) for v in value if str(v) not in self.choices.field.empty_values
510        }
511        if not self.is_required and not self.allow_multiple_selected:
512            default[1].append(self.create_option(name, "", "", False, 0))
513        choices = (
514            (obj.pk, self.choices.field.label_from_instance(obj))
515            for obj in self.choices.queryset.using(self.db).filter(
516                pk__in=selected_choices
517            )
518        )
519        for option_value, option_label in choices:
520            selected = str(option_value) in value and (
521                has_selected is False or self.allow_multiple_selected
522            )
523            has_selected |= selected
524            index = len(default[1])
525            subgroup = default[1]
526            subgroup.append(
527                self.create_option(
528                    name, option_value, option_label, selected_choices, index
529                )
530            )
531        return groups
532
533    @property
534    def media(self):
535        extra = "" if settings.DEBUG else ".min"
536        i18n_name = SELECT2_TRANSLATIONS.get(get_language())
537        i18n_file = (
538            ("admin/js/vendor/select2/i18n/%s.js" % i18n_name,) if i18n_name else ()
539        )
540        return forms.Media(
541            js=(
542                "admin/js/vendor/jquery/jquery%s.js" % extra,
543                "admin/js/vendor/select2/select2.full%s.js" % extra,
544            )
545            + i18n_file
546            + ("admin/js/jquery.init.js", "admin/js/autocomplete.js",),
547            css={
548                "screen": (
549                    "admin/css/vendor/select2/select2%s.css" % extra,
550                    "admin/css/autocomplete.css",
551                ),
552            },
553        )
554
555
556class AutocompleteSelect(AutocompleteMixin, forms.Select):
557    pass
558
559
560class AutocompleteSelectMultiple(AutocompleteMixin, forms.SelectMultiple):
561    pass

tests/auth_tests/test_models.py

  1from unittest import mock
  2
  3from django.conf.global_settings import PASSWORD_HASHERS
  4from django.contrib.auth import get_user_model
  5from django.contrib.auth.backends import ModelBackend
  6from django.contrib.auth.base_user import AbstractBaseUser
  7from django.contrib.auth.hashers import get_hasher
  8from django.contrib.auth.models import (
  9    AbstractUser,
 10    AnonymousUser,
 11    Group,
 12    Permission,
 13    User,
 14    UserManager,
 15)
 16from django.contrib.contenttypes.models import ContentType
 17from django.core import mail
 18from django.db.models.signals import post_save
 19from django.test import SimpleTestCase, TestCase, override_settings
 20
 21from .models import IntegerUsernameUser
 22from .models.with_custom_email_field import CustomEmailField
 23
 24
 25class NaturalKeysTestCase(TestCase):
 26    def test_user_natural_key(self):
 27        staff_user = User.objects.create_user(username="staff")
 28        self.assertEqual(User.objects.get_by_natural_key("staff"), staff_user)
 29        self.assertEqual(staff_user.natural_key(), ("staff",))
 30
 31    def test_group_natural_key(self):
 32        users_group = Group.objects.create(name="users")
 33        self.assertEqual(Group.objects.get_by_natural_key("users"), users_group)
 34
 35
 36class LoadDataWithoutNaturalKeysTestCase(TestCase):
 37    fixtures = ["regular.json"]
 38
 39    def test_user_is_created_and_added_to_group(self):
 40        user = User.objects.get(username="my_username")
 41        group = Group.objects.get(name="my_group")
 42        self.assertEqual(group, user.groups.get())
 43
 44
 45class LoadDataWithNaturalKeysTestCase(TestCase):
 46    fixtures = ["natural.json"]
 47
 48    def test_user_is_created_and_added_to_group(self):
 49        user = User.objects.get(username="my_username")
 50        group = Group.objects.get(name="my_group")
 51        self.assertEqual(group, user.groups.get())
 52
 53
 54class LoadDataWithNaturalKeysAndMultipleDatabasesTestCase(TestCase):
 55    databases = {"default", "other"}
 56
 57    def test_load_data_with_user_permissions(self):
 58        # Create test contenttypes for both databases
 59        default_objects = [
 60            ContentType.objects.db_manager("default").create(
 61                model="examplemodela", app_label="app_a",
 62            ),
 63            ContentType.objects.db_manager("default").create(
 64                model="examplemodelb", app_label="app_b",
 65            ),
 66        ]
 67        other_objects = [
 68            ContentType.objects.db_manager("other").create(
 69                model="examplemodelb", app_label="app_b",
 70            ),
 71            ContentType.objects.db_manager("other").create(
 72                model="examplemodela", app_label="app_a",
 73            ),
 74        ]
 75
 76        # Now we create the test UserPermission
 77        Permission.objects.db_manager("default").create(
 78            name="Can delete example model b",
 79            codename="delete_examplemodelb",
 80            content_type=default_objects[1],
 81        )
 82        Permission.objects.db_manager("other").create(
 83            name="Can delete example model b",
 84            codename="delete_examplemodelb",
 85            content_type=other_objects[0],
 86        )
 87
 88        perm_default = Permission.objects.get_by_natural_key(
 89            "delete_examplemodelb", "app_b", "examplemodelb",
 90        )
 91
 92        perm_other = Permission.objects.db_manager("other").get_by_natural_key(
 93            "delete_examplemodelb", "app_b", "examplemodelb",
 94        )
 95
 96        self.assertEqual(perm_default.content_type_id, default_objects[1].id)
 97        self.assertEqual(perm_other.content_type_id, other_objects[0].id)
 98
 99
100class UserManagerTestCase(TestCase):
101    def test_create_user(self):
102        email_lowercase = "normal@normal.com"
103        user = User.objects.create_user("user", email_lowercase)
104        self.assertEqual(user.email, email_lowercase)
105        self.assertEqual(user.username, "user")
106        self.assertFalse(user.has_usable_password())
107
108    def test_create_user_email_domain_normalize_rfc3696(self):
109        # According to https://tools.ietf.org/html/rfc3696#section-3
110        # the "@" symbol can be part of the local part of an email address
111        returned = UserManager.normalize_email(r"Abc\@DEF@EXAMPLE.com")
112        self.assertEqual(returned, r"Abc\@DEF@example.com")
113
114    def test_create_user_email_domain_normalize(self):
115        returned = UserManager.normalize_email("normal@DOMAIN.COM")
116        self.assertEqual(returned, "normal@domain.com")
117
118    def test_create_user_email_domain_normalize_with_whitespace(self):
119        returned = UserManager.normalize_email(r"email\ with_whitespace@D.COM")
120        self.assertEqual(returned, r"email\ with_whitespace@d.com")
121
122    def test_empty_username(self):
123        with self.assertRaisesMessage(ValueError, "The given username must be set"):
124            User.objects.create_user(username="")
125
126    def test_create_user_is_staff(self):
127        email = "normal@normal.com"
128        user = User.objects.create_user("user", email, is_staff=True)
129        self.assertEqual(user.email, email)
130        self.assertEqual(user.username, "user")
131        self.assertTrue(user.is_staff)
132
133    def test_create_super_user_raises_error_on_false_is_superuser(self):
134        with self.assertRaisesMessage(
135            ValueError, "Superuser must have is_superuser=True."
136        ):
137            User.objects.create_superuser(
138                username="test",
139                email="test@test.com",
140                password="test",
141                is_superuser=False,
142            )
143
144    def test_create_superuser_raises_error_on_false_is_staff(self):
145        with self.assertRaisesMessage(ValueError, "Superuser must have is_staff=True."):
146            User.objects.create_superuser(
147                username="test", email="test@test.com", password="test", is_staff=False,
148            )
149
150    def test_make_random_password(self):
151        allowed_chars = "abcdefg"
152        password = UserManager().make_random_password(5, allowed_chars)
153        self.assertEqual(len(password), 5)
154        for char in password:
155            self.assertIn(char, allowed_chars)
156
157
158class AbstractBaseUserTests(SimpleTestCase):
159    def test_has_usable_password(self):
160        """
161        Passwords are usable even if they don't correspond to a hasher in
162        settings.PASSWORD_HASHERS.
163        """
164        self.assertIs(User(password="some-gibbberish").has_usable_password(), True)
165
166    def test_normalize_username(self):
167        self.assertEqual(IntegerUsernameUser().normalize_username(123), 123)
168
169    def test_clean_normalize_username(self):
170        # The normalization happens in AbstractBaseUser.clean()
171        ohm_username = "iamtheΩ"  # U+2126 OHM SIGN
172        for model in ("auth.User", "auth_tests.CustomUser"):
173            with self.subTest(model=model), self.settings(AUTH_USER_MODEL=model):
174                User = get_user_model()
175                user = User(**{User.USERNAME_FIELD: ohm_username, "password": "foo"})
176                user.clean()
177                username = user.get_username()
178                self.assertNotEqual(username, ohm_username)
179                self.assertEqual(
180                    username, "iamtheΩ"
181                )  # U+03A9 GREEK CAPITAL LETTER OMEGA
182
183    def test_default_email(self):
184        user = AbstractBaseUser()
185        self.assertEqual(user.get_email_field_name(), "email")
186
187    def test_custom_email(self):
188        user = CustomEmailField()
189        self.assertEqual(user.get_email_field_name(), "email_address")
190
191
192class AbstractUserTestCase(TestCase):
193    def test_email_user(self):
194        # valid send_mail parameters
195        kwargs = {
196            "fail_silently": False,
197            "auth_user": None,
198            "auth_password": None,
199            "connection": None,
200            "html_message": None,
201        }
202        abstract_user = AbstractUser(email="foo@bar.com")
203        abstract_user.email_user(
204            subject="Subject here",
205            message="This is a message",
206            from_email="from@domain.com",
207            **kwargs
208        )
209        self.assertEqual(len(mail.outbox), 1)
210        message = mail.outbox[0]
211        self.assertEqual(message.subject, "Subject here")
212        self.assertEqual(message.body, "This is a message")
213        self.assertEqual(message.from_email, "from@domain.com")
214        self.assertEqual(message.to, [abstract_user.email])
215
216    def test_last_login_default(self):
217        user1 = User.objects.create(username="user1")
218        self.assertIsNone(user1.last_login)
219
220        user2 = User.objects.create_user(username="user2")
221        self.assertIsNone(user2.last_login)
222
223    def test_user_clean_normalize_email(self):
224        user = User(username="user", password="foo", email="foo@BAR.com")
225        user.clean()
226        self.assertEqual(user.email, "foo@bar.com")
227
228    def test_user_double_save(self):
229        """
230        Calling user.save() twice should trigger password_changed() once.
231        """
232        user = User.objects.create_user(username="user", password="foo")
233        user.set_password("bar")
234        with mock.patch(
235            "django.contrib.auth.password_validation.password_changed"
236        ) as pw_changed:
237            user.save()
238            self.assertEqual(pw_changed.call_count, 1)
239            user.save()
240            self.assertEqual(pw_changed.call_count, 1)
241
242    @override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
243    def test_check_password_upgrade(self):
244        """
245        password_changed() shouldn't be called if User.check_password()
246        triggers a hash iteration upgrade.
247        """
248        user = User.objects.create_user(username="user", password="foo")
249        initial_password = user.password
250        self.assertTrue(user.check_password("foo"))
251        hasher = get_hasher("default")
252        self.assertEqual("pbkdf2_sha256", hasher.algorithm)
253
254        old_iterations = hasher.iterations
255        try:
256            # Upgrade the password iterations
257            hasher.iterations = old_iterations + 1
258            with mock.patch(
259                "django.contrib.auth.password_validation.password_changed"
260            ) as pw_changed:
261                user.check_password("foo")
262                self.assertEqual(pw_changed.call_count, 0)
263            self.assertNotEqual(initial_password, user.password)
264        finally:
265            hasher.iterations = old_iterations
266
267
268class CustomModelBackend(ModelBackend):
269    def with_perm(
270        self, perm, is_active=True, include_superusers=True, backend=None, obj=None
271    ):
272        if obj is not None and obj.username == "charliebrown":
273            return User.objects.filter(pk=obj.pk)
274        return User.objects.filter(username__startswith="charlie")
275
276
277class UserWithPermTestCase(TestCase):
278    @classmethod
279    def setUpTestData(cls):
280        content_type = ContentType.objects.get_for_model(Group)
281        cls.permission = Permission.objects.create(
282            name="test", content_type=content_type, codename="test",
283        )
284        # User with permission.
285        cls.user1 = User.objects.create_user("user 1", "foo@example.com")
286        cls.user1.user_permissions.add(cls.permission)
287        # User with group permission.
288        group1 = Group.objects.create(name="group 1")
289        group1.permissions.add(cls.permission)
290        group2 = Group.objects.create(name="group 2")
291        group2.permissions.add(cls.permission)
292        cls.user2 = User.objects.create_user("user 2", "bar@example.com")
293        cls.user2.groups.add(group1, group2)
294        # Users without permissions.
295        cls.user_charlie = User.objects.create_user("charlie", "charlie@example.com")
296        cls.user_charlie_b = User.objects.create_user(
297            "charliebrown", "charlie@brown.com"
298        )
299        # Superuser.
300        cls.superuser = User.objects.create_superuser(
301            "superuser", "superuser@example.com", "superpassword",
302        )
303        # Inactive user with permission.
304        cls.inactive_user = User.objects.create_user(
305            "inactive_user", "baz@example.com", is_active=False,
306        )
307        cls.inactive_user.user_permissions.add(cls.permission)
308
309    def test_invalid_permission_name(self):
310        msg = "Permission name should be in the form app_label.permission_codename."
311        for perm in ("nodots", "too.many.dots", "...", ""):
312            with self.subTest(perm), self.assertRaisesMessage(ValueError, msg):
313                User.objects.with_perm(perm)
314
315    def test_invalid_permission_type(self):
316        msg = "The `perm` argument must be a string or a permission instance."
317        for perm in (b"auth.test", object(), None):
318            with self.subTest(perm), self.assertRaisesMessage(TypeError, msg):
319                User.objects.with_perm(perm)
320
321    def test_invalid_backend_type(self):
322        msg = "backend must be a dotted import path string (got %r)."
323        for backend in (b"auth_tests.CustomModelBackend", object()):
324            with self.subTest(backend):
325                with self.assertRaisesMessage(TypeError, msg % backend):
326                    User.objects.with_perm("auth.test", backend=backend)
327
328    def test_basic(self):
329        active_users = [self.user1, self.user2]
330        tests = [
331            ({}, [*active_users, self.superuser]),
332            ({"obj": self.user1}, []),
333            # Only inactive users.
334            ({"is_active": False}, [self.inactive_user]),
335            # All users.
336            ({"is_active": None}, [*active_users, self.superuser, self.inactive_user]),
337            # Exclude superusers.
338            ({"include_superusers": False}, active_users),
339            ({"include_superusers": False, "is_active": False}, [self.inactive_user],),
340            (
341                {"include_superusers": False, "is_active": None},
342                [*active_users, self.inactive_user],
343            ),
344        ]
345        for kwargs, expected_users in tests:
346            for perm in ("auth.test", self.permission):
347                with self.subTest(perm=perm, **kwargs):
348                    self.assertCountEqual(
349                        User.objects.with_perm(perm, **kwargs), expected_users,
350                    )
351
352    @override_settings(
353        AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.BaseBackend"]
354    )
355    def test_backend_without_with_perm(self):
356        self.assertSequenceEqual(User.objects.with_perm("auth.test"), [])
357
358    def test_nonexistent_permission(self):
359        self.assertSequenceEqual(User.objects.with_perm("auth.perm"), [self.superuser])
360
361    def test_nonexistent_backend(self):
362        with self.assertRaises(ImportError):
363            User.objects.with_perm(
364                "auth.test", backend="invalid.backend.CustomModelBackend",
365            )
366
367    @override_settings(
368        AUTHENTICATION_BACKENDS=["auth_tests.test_models.CustomModelBackend"]
369    )
370    def test_custom_backend(self):
371        for perm in ("auth.test", self.permission):
372            with self.subTest(perm):
373                self.assertCountEqual(
374                    User.objects.with_perm(perm),
375                    [self.user_charlie, self.user_charlie_b],
376                )
377
378    @override_settings(
379        AUTHENTICATION_BACKENDS=["auth_tests.test_models.CustomModelBackend"]
380    )
381    def test_custom_backend_pass_obj(self):
382        for perm in ("auth.test", self.permission):
383            with self.subTest(perm):
384                self.assertSequenceEqual(
385                    User.objects.with_perm(perm, obj=self.user_charlie_b),
386                    [self.user_charlie_b],
387                )
388
389    @override_settings(
390        AUTHENTICATION_BACKENDS=[
391            "auth_tests.test_models.CustomModelBackend",
392            "django.contrib.auth.backends.ModelBackend",
393        ]
394    )
395    def test_multiple_backends(self):
396        msg = (
397            "You have multiple authentication backends configured and "
398            "therefore must provide the `backend` argument."
399        )
400        with self.assertRaisesMessage(ValueError, msg):
401            User.objects.with_perm("auth.test")
402
403        backend = "auth_tests.test_models.CustomModelBackend"
404        self.assertCountEqual(
405            User.objects.with_perm("auth.test", backend=backend),
406            [self.user_charlie, self.user_charlie_b],
407        )
408
409
410class IsActiveTestCase(TestCase):
411    """
412    Tests the behavior of the guaranteed is_active attribute
413    """
414
415    def test_builtin_user_isactive(self):
416        user = User.objects.create(username="foo", email="foo@bar.com")
417        # is_active is true by default
418        self.assertIs(user.is_active, True)
419        user.is_active = False
420        user.save()
421        user_fetched = User.objects.get(pk=user.pk)
422        # the is_active flag is saved
423        self.assertFalse(user_fetched.is_active)
424
425    @override_settings(AUTH_USER_MODEL="auth_tests.IsActiveTestUser1")
426    def test_is_active_field_default(self):
427        """
428        tests that the default value for is_active is provided
429        """
430        UserModel = get_user_model()
431        user = UserModel(username="foo")
432        self.assertIs(user.is_active, True)
433        # you can set the attribute - but it will not save
434        user.is_active = False
435        # there should be no problem saving - but the attribute is not saved
436        user.save()
437        user_fetched = UserModel._default_manager.get(pk=user.pk)
438        # the attribute is always true for newly retrieved instance
439        self.assertIs(user_fetched.is_active, True)
440
441
442class TestCreateSuperUserSignals(TestCase):
443    """
444    Simple test case for ticket #20541
445    """
446
447    def post_save_listener(self, *args, **kwargs):
448        self.signals_count += 1
449
450    def setUp(self):
451        self.signals_count = 0
452        post_save.connect(self.post_save_listener, sender=User)
453
454    def tearDown(self):
455        post_save.disconnect(self.post_save_listener, sender=User)
456
457    def test_create_user(self):
458        User.objects.create_user("JohnDoe")
459        self.assertEqual(self.signals_count, 1)
460
461    def test_create_superuser(self):
462        User.objects.create_superuser("JohnDoe", "mail@example.com", "1")
463        self.assertEqual(self.signals_count, 1)
464
465
466class AnonymousUserTests(SimpleTestCase):
467    no_repr_msg = "Django doesn't provide a DB representation for AnonymousUser."
468
469    def setUp(self):
470        self.user = AnonymousUser()
471
472    def test_properties(self):
473        self.assertIsNone(self.user.pk)
474        self.assertEqual(self.user.username, "")
475        self.assertEqual(self.user.get_username(), "")
476        self.assertIs(self.user.is_anonymous, True)
477        self.assertIs(self.user.is_authenticated, False)
478        self.assertIs(self.user.is_staff, False)
479        self.assertIs(self.user.is_active, False)
480        self.assertIs(self.user.is_superuser, False)
481        self.assertEqual(self.user.groups.all().count(), 0)
482        self.assertEqual(self.user.user_permissions.all().count(), 0)
483        self.assertEqual(self.user.get_user_permissions(), set())
484        self.assertEqual(self.user.get_group_permissions(), set())
485
486    def test_str(self):
487        self.assertEqual(str(self.user), "AnonymousUser")
488
489    def test_eq(self):
490        self.assertEqual(self.user, AnonymousUser())
491        self.assertNotEqual(self.user, User("super", "super@example.com", "super"))
492
493    def test_hash(self):
494        self.assertEqual(hash(self.user), 1)
495
496    def test_int(self):
497        msg = (
498            "Cannot cast AnonymousUser to int. Are you trying to use it in "
499            "place of User?"
500        )
501        with self.assertRaisesMessage(TypeError, msg):
502            int(self.user)
503
504    def test_delete(self):
505        with self.assertRaisesMessage(NotImplementedError, self.no_repr_msg):
506            self.user.delete()
507
508    def test_save(self):
509        with self.assertRaisesMessage(NotImplementedError, self.no_repr_msg):
510            self.user.save()
511
512    def test_set_password(self):
513        with self.assertRaisesMessage(NotImplementedError, self.no_repr_msg):
514            self.user.set_password("password")
515
516    def test_check_password(self):
517        with self.assertRaisesMessage(NotImplementedError, self.no_repr_msg):
518            self.user.check_password("password")
519
520
521class GroupTests(SimpleTestCase):
522    def test_str(self):
523        g = Group(name="Users")
524        self.assertEqual(str(g), "Users")
525
526
527class PermissionTests(TestCase):
528    def test_str(self):
529        p = Permission.objects.get(codename="view_customemailfield")
530        self.assertEqual(
531            str(p), "auth_tests | custom email field | Can view custom email field"
532        )

tests/test_client/tests.py

  1"""
  2Testing using the Test Client
  3
  4The test client is a class that can act like a simple
  5browser for testing purposes.
  6
  7It allows the user to compose GET and POST requests, and
  8obtain the response that the server gave to those requests.
  9The server Response objects are annotated with the details
 10of the contexts and templates that were rendered during the
 11process of serving the request.
 12
 13``Client`` objects are stateful - they will retain cookie (and
 14thus session) details for the lifetime of the ``Client`` instance.
 15
 16This is not intended as a replacement for Twill, Selenium, or
 17other browser automation frameworks - it is here to allow
 18testing against the contexts and templates produced by a view,
 19rather than the HTML rendered to the end-user.
 20
 21"""
 22import itertools
 23import tempfile
 24from unittest import mock
 25
 26from django.contrib.auth.models import User
 27from django.core import mail
 28from django.http import HttpResponse
 29from django.test import (
 30    Client,
 31    RequestFactory,
 32    SimpleTestCase,
 33    TestCase,
 34    override_settings,
 35)
 36from django.urls import reverse_lazy
 37
 38from .views import TwoArgException, get_view, post_view, trace_view
 39
 40
 41@override_settings(ROOT_URLCONF="test_client.urls")
 42class ClientTest(TestCase):
 43    @classmethod
 44    def setUpTestData(cls):
 45        cls.u1 = User.objects.create_user(username="testclient", password="password")
 46        cls.u2 = User.objects.create_user(
 47            username="inactive", password="password", is_active=False
 48        )
 49
 50    def test_get_view(self):
 51        "GET a view"
 52        # The data is ignored, but let's check it doesn't crash the system
 53        # anyway.
 54        data = {"var": "\xf2"}
 55        response = self.client.get("/get_view/", data)
 56
 57        # Check some response details
 58        self.assertContains(response, "This is a test")
 59        self.assertEqual(response.context["var"], "\xf2")
 60        self.assertEqual(response.templates[0].name, "GET Template")
 61
 62    def test_query_string_encoding(self):
 63        # WSGI requires latin-1 encoded strings.
 64        response = self.client.get("/get_view/?var=1\ufffd")
 65        self.assertEqual(response.context["var"], "1\ufffd")
 66
 67    def test_get_data_none(self):
 68        msg = (
 69            "Cannot encode None for key 'value' in a query string. Did you "
 70            "mean to pass an empty string or omit the value?"
 71        )
 72        with self.assertRaisesMessage(TypeError, msg):
 73            self.client.get("/get_view/", {"value": None})
 74
 75    def test_get_post_view(self):
 76        "GET a view that normally expects POSTs"
 77        response = self.client.get("/post_view/", {})
 78
 79        # Check some response details
 80        self.assertEqual(response.status_code, 200)
 81        self.assertEqual(response.templates[0].name, "Empty GET Template")
 82        self.assertTemplateUsed(response, "Empty GET Template")
 83        self.assertTemplateNotUsed(response, "Empty POST Template")
 84
 85    def test_empty_post(self):
 86        "POST an empty dictionary to a view"
 87        response = self.client.post("/post_view/", {})
 88
 89        # Check some response details
 90        self.assertEqual(response.status_code, 200)
 91        self.assertEqual(response.templates[0].name, "Empty POST Template")
 92        self.assertTemplateNotUsed(response, "Empty GET Template")
 93        self.assertTemplateUsed(response, "Empty POST Template")
 94
 95    def test_post(self):
 96        "POST some data to a view"
 97        post_data = {"value": 37}
 98        response = self.client.post("/post_view/", post_data)
 99
100        # Check some response details
101        self.assertEqual(response.status_code, 200)
102        self.assertEqual(response.context["data"], "37")
103        self.assertEqual(response.templates[0].name, "POST Template")
104        self.assertContains(response, "Data received")
105
106    def test_post_data_none(self):
107        msg = (
108            "Cannot encode None for key 'value' as POST data. Did you mean "
109            "to pass an empty string or omit the value?"
110        )
111        with self.assertRaisesMessage(TypeError, msg):
112            self.client.post("/post_view/", {"value": None})
113
114    def test_json_serialization(self):
115        """The test client serializes JSON data."""
116        methods = ("post", "put", "patch", "delete")
117        tests = (
118            ({"value": 37}, {"value": 37}),
119            ([37, True], [37, True]),
120            ((37, False), [37, False]),
121        )
122        for method in methods:
123            with self.subTest(method=method):
124                for data, expected in tests:
125                    with self.subTest(data):
126                        client_method = getattr(self.client, method)
127                        method_name = method.upper()
128                        response = client_method(
129                            "/json_view/", data, content_type="application/json"
130                        )
131                        self.assertEqual(response.status_code, 200)
132                        self.assertEqual(response.context["data"], expected)
133                        self.assertContains(response, "Viewing %s page." % method_name)
134
135    def test_json_encoder_argument(self):
136        """The test Client accepts a json_encoder."""
137        mock_encoder = mock.MagicMock()
138        mock_encoding = mock.MagicMock()
139        mock_encoder.return_value = mock_encoding
140        mock_encoding.encode.return_value = '{"value": 37}'
141
142        client = self.client_class(json_encoder=mock_encoder)
143        # Vendored tree JSON content types are accepted.
144        client.post(
145            "/json_view/", {"value": 37}, content_type="application/vnd.api+json"
146        )
147        self.assertTrue(mock_encoder.called)
148        self.assertTrue(mock_encoding.encode.called)
149
150    def test_put(self):
151        response = self.client.put("/put_view/", {"foo": "bar"})
152        self.assertEqual(response.status_code, 200)
153        self.assertEqual(response.templates[0].name, "PUT Template")
154        self.assertEqual(response.context["data"], "{'foo': 'bar'}")
155        self.assertEqual(response.context["Content-Length"], "14")
156
157    def test_trace(self):
158        """TRACE a view"""
159        response = self.client.trace("/trace_view/")
160        self.assertEqual(response.status_code, 200)
161        self.assertEqual(response.context["method"], "TRACE")
162        self.assertEqual(response.templates[0].name, "TRACE Template")
163
164    def test_response_headers(self):
165        "Check the value of HTTP headers returned in a response"
166        response = self.client.get("/header_view/")
167
168        self.assertEqual(response["X-DJANGO-TEST"], "Slartibartfast")
169
170    def test_response_attached_request(self):
171        """
172        The returned response has a ``request`` attribute with the originating
173        environ dict and a ``wsgi_request`` with the originating WSGIRequest.
174        """
175        response = self.client.get("/header_view/")
176
177        self.assertTrue(hasattr(response, "request"))
178        self.assertTrue(hasattr(response, "wsgi_request"))
179        for key, value in response.request.items():
180            self.assertIn(key, response.wsgi_request.environ)
181            self.assertEqual(response.wsgi_request.environ[key], value)
182
183    def test_response_resolver_match(self):
184        """
185        The response contains a ResolverMatch instance.
186        """
187        response = self.client.get("/header_view/")
188        self.assertTrue(hasattr(response, "resolver_match"))
189
190    def test_response_resolver_match_redirect_follow(self):
191        """
192        The response ResolverMatch instance contains the correct
193        information when following redirects.
194        """
195        response = self.client.get("/redirect_view/", follow=True)
196        self.assertEqual(response.resolver_match.url_name, "get_view")
197
198    def test_response_resolver_match_regular_view(self):
199        """
200        The response ResolverMatch instance contains the correct
201        information when accessing a regular view.
202        """
203        response = self.client.get("/get_view/")
204        self.assertEqual(response.resolver_match.url_name, "get_view")
205
206    def test_raw_post(self):
207        "POST raw data (with a content type) to a view"
208        test_doc = """<?xml version="1.0" encoding="utf-8"?>
209        <library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>
210        """
211        response = self.client.post(
212            "/raw_post_view/", test_doc, content_type="text/xml"
213        )
214        self.assertEqual(response.status_code, 200)
215        self.assertEqual(response.templates[0].name, "Book template")
216        self.assertEqual(response.content, b"Blink - Malcolm Gladwell")
217
218    def test_insecure(self):
219        "GET a URL through http"
220        response = self.client.get("/secure_view/", secure=False)
221        self.assertFalse(response.test_was_secure_request)
222        self.assertEqual(response.test_server_port, "80")
223
224    def test_secure(self):
225        "GET a URL through https"
226        response = self.client.get("/secure_view/", secure=True)
227        self.assertTrue(response.test_was_secure_request)
228        self.assertEqual(response.test_server_port, "443")
229
230    def test_redirect(self):
231        "GET a URL that redirects elsewhere"
232        response = self.client.get("/redirect_view/")
233        self.assertRedirects(response, "/get_view/")
234
235    def test_redirect_with_query(self):
236        "GET a URL that redirects with given GET parameters"
237        response = self.client.get("/redirect_view/", {"var": "value"})
238        self.assertRedirects(response, "/get_view/?var=value")
239
240    def test_redirect_with_query_ordering(self):
241        """assertRedirects() ignores the order of query string parameters."""
242        response = self.client.get("/redirect_view/", {"var": "value", "foo": "bar"})
243        self.assertRedirects(response, "/get_view/?var=value&foo=bar")
244        self.assertRedirects(response, "/get_view/?foo=bar&var=value")
245
246    def test_permanent_redirect(self):
247        "GET a URL that redirects permanently elsewhere"
248        response = self.client.get("/permanent_redirect_view/")
249        self.assertRedirects(response, "/get_view/", status_code=301)
250
251    def test_temporary_redirect(self):
252        "GET a URL that does a non-permanent redirect"
253        response = self.client.get("/temporary_redirect_view/")
254        self.assertRedirects(response, "/get_view/", status_code=302)
255
256    def test_redirect_to_strange_location(self):
257        "GET a URL that redirects to a non-200 page"
258        response = self.client.get("/double_redirect_view/")
259        # The response was a 302, and that the attempt to get the redirection
260        # location returned 301 when retrieved
261        self.assertRedirects(
262            response, "/permanent_redirect_view/", target_status_code=301
263        )
264
265    def test_follow_redirect(self):
266        "A URL that redirects can be followed to termination."
267        response = self.client.get("/double_redirect_view/", follow=True)
268        self.assertRedirects(
269            response, "/get_view/", status_code=302, target_status_code=200
270        )
271        self.assertEqual(len(response.redirect_chain), 2)
272
273    def test_follow_relative_redirect(self):
274        "A URL with a relative redirect can be followed."
275        response = self.client.get("/accounts/", follow=True)
276        self.assertEqual(response.status_code, 200)
277        self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
278
279    def test_follow_relative_redirect_no_trailing_slash(self):
280        "A URL with a relative redirect with no trailing slash can be followed."
281        response = self.client.get("/accounts/no_trailing_slash", follow=True)
282        self.assertEqual(response.status_code, 200)
283        self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
284
285    def test_follow_307_and_308_redirect(self):
286        """
287        A 307 or 308 redirect preserves the request method after the redirect.
288        """
289        methods = ("get", "post", "head", "options", "put", "patch", "delete", "trace")
290        codes = (307, 308)
291        for method, code in itertools.product(methods, codes):
292            with self.subTest(method=method, code=code):
293                req_method = getattr(self.client, method)
294                response = req_method(
295                    "/redirect_view_%s/" % code, data={"value": "test"}, follow=True
296                )
297                self.assertEqual(response.status_code, 200)
298                self.assertEqual(response.request["PATH_INFO"], "/post_view/")
299                self.assertEqual(response.request["REQUEST_METHOD"], method.upper())
300
301    def test_follow_307_and_308_preserves_post_data(self):
302        for code in (307, 308):
303            with self.subTest(code=code):
304                response = self.client.post(
305                    "/redirect_view_%s/" % code, data={"value": "test"}, follow=True
306                )
307                self.assertContains(response, "test is the value")
308
309    def test_follow_307_and_308_preserves_put_body(self):
310        for code in (307, 308):
311            with self.subTest(code=code):
312                response = self.client.put(
313                    "/redirect_view_%s/?to=/put_view/" % code, data="a=b", follow=True
314                )
315                self.assertContains(response, "a=b is the body")
316
317    def test_follow_307_and_308_preserves_get_params(self):
318        data = {"var": 30, "to": "/get_view/"}
319        for code in (307, 308):
320            with self.subTest(code=code):
321                response = self.client.get(
322                    "/redirect_view_%s/" % code, data=data, follow=True
323                )
324                self.assertContains(response, "30 is the value")
325
326    def test_redirect_http(self):
327        "GET a URL that redirects to an http URI"
328        response = self.client.get("/http_redirect_view/", follow=True)
329        self.assertFalse(response.test_was_secure_request)
330
331    def test_redirect_https(self):
332        "GET a URL that redirects to an https URI"
333        response = self.client.get("/https_redirect_view/", follow=True)
334        self.assertTrue(response.test_was_secure_request)
335
336    def test_notfound_response(self):
337        "GET a URL that responds as '404:Not Found'"
338        response = self.client.get("/bad_view/")
339        self.assertContains(response, "MAGIC", status_code=404)
340
341    def test_valid_form(self):
342        "POST valid data to a form"
343        post_data = {
344            "text": "Hello World",
345            "email": "foo@example.com",
346            "value": 37,
347            "single": "b",
348            "multi": ("b", "c", "e"),
349        }
350        response = self.client.post("/form_view/", post_data)
351        self.assertEqual(response.status_code, 200)
352        self.assertTemplateUsed(response, "Valid POST Template")
353
354    def test_valid_form_with_hints(self):
355        "GET a form, providing hints in the GET data"
356        hints = {"text": "Hello World", "multi": ("b", "c", "e")}
357        response = self.client.get("/form_view/", data=hints)
358        self.assertEqual(response.status_code, 200)
359        self.assertTemplateUsed(response, "Form GET Template")
360        # The multi-value data has been rolled out ok
361        self.assertContains(response, "Select a valid choice.", 0)
362
363    def test_incomplete_data_form(self):
364        "POST incomplete data to a form"
365        post_data = {"text": "Hello World", "value": 37}
366        response = self.client.post("/form_view/", post_data)
367        self.assertContains(response, "This field is required.", 3)
368        self.assertEqual(response.status_code, 200)
369        self.assertTemplateUsed(response, "Invalid POST Template")
370
371        self.assertFormError(response, "form", "email", "This field is required.")
372        self.assertFormError(response, "form", "single", "This field is required.")
373        self.assertFormError(response, "form", "multi", "This field is required.")
374
375    def test_form_error(self):
376        "POST erroneous data to a form"
377        post_data = {
378            "text": "Hello World",
379            "email": "not an email address",
380            "value": 37,
381            "single": "b",
382            "multi": ("b", "c", "e"),
383        }
384        response = self.client.post("/form_view/", post_data)
385        self.assertEqual(response.status_code, 200)
386        self.assertTemplateUsed(response, "Invalid POST Template")
387
388        self.assertFormError(response, "form", "email", "Enter a valid email address.")
389
390    def test_valid_form_with_template(self):
391        "POST valid data to a form using multiple templates"
392        post_data = {
393            "text": "Hello World",
394            "email": "foo@example.com",
395            "value": 37,
396            "single": "b",
397            "multi": ("b", "c", "e"),
398        }
399        response = self.client.post("/form_view_with_template/", post_data)
400        self.assertContains(response, "POST data OK")
401        self.assertTemplateUsed(response, "form_view.html")
402        self.assertTemplateUsed(response, "base.html")
403        self.assertTemplateNotUsed(response, "Valid POST Template")
404
405    def test_incomplete_data_form_with_template(self):
406        "POST incomplete data to a form using multiple templates"
407        post_data = {"text": "Hello World", "value": 37}
408        response = self.client.post("/form_view_with_template/", post_data)
409        self.assertContains(response, "POST data has errors")
410        self.assertTemplateUsed(response, "form_view.html")
411        self.assertTemplateUsed(response, "base.html")
412        self.assertTemplateNotUsed(response, "Invalid POST Template")
413
414        self.assertFormError(response, "form", "email", "This field is required.")
415        self.assertFormError(response, "form", "single", "This field is required.")
416        self.assertFormError(response, "form", "multi", "This field is required.")
417
418    def test_form_error_with_template(self):
419        "POST erroneous data to a form using multiple templates"
420        post_data = {
421            "text": "Hello World",
422            "email": "not an email address",
423            "value": 37,
424            "single": "b",
425            "multi": ("b", "c", "e"),
426        }
427        response = self.client.post("/form_view_with_template/", post_data)
428        self.assertContains(response, "POST data has errors")
429        self.assertTemplateUsed(response, "form_view.html")
430        self.assertTemplateUsed(response, "base.html")
431        self.assertTemplateNotUsed(response, "Invalid POST Template")
432
433        self.assertFormError(response, "form", "email", "Enter a valid email address.")
434
435    def test_unknown_page(self):
436        "GET an invalid URL"
437        response = self.client.get("/unknown_view/")
438
439        # The response was a 404
440        self.assertEqual(response.status_code, 404)
441
442    def test_url_parameters(self):
443        "Make sure that URL ;-parameters are not stripped."
444        response = self.client.get("/unknown_view/;some-parameter")
445
446        # The path in the response includes it (ignore that it's a 404)
447        self.assertEqual(response.request["PATH_INFO"], "/unknown_view/;some-parameter")
448
449    def test_view_with_login(self):
450        "Request a page that is protected with @login_required"
451
452        # Get the page without logging in. Should result in 302.
453        response = self.client.get("/login_protected_view/")
454        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
455
456        # Log in
457        login = self.client.login(username="testclient", password="password")
458        self.assertTrue(login, "Could not log in")
459
460        # Request a page that requires a login
461        response = self.client.get("/login_protected_view/")
462        self.assertEqual(response.status_code, 200)
463        self.assertEqual(response.context["user"].username, "testclient")
464
465    @override_settings(
466        INSTALLED_APPS=["django.contrib.auth"],
467        SESSION_ENGINE="django.contrib.sessions.backends.file",
468    )
469    def test_view_with_login_when_sessions_app_is_not_installed(self):
470        self.test_view_with_login()
471
472    def test_view_with_force_login(self):
473        "Request a page that is protected with @login_required"
474        # Get the page without logging in. Should result in 302.
475        response = self.client.get("/login_protected_view/")
476        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
477
478        # Log in
479        self.client.force_login(self.u1)
480
481        # Request a page that requires a login
482        response = self.client.get("/login_protected_view/")
483        self.assertEqual(response.status_code, 200)
484        self.assertEqual(response.context["user"].username, "testclient")
485
486    def test_view_with_method_login(self):
487        "Request a page that is protected with a @login_required method"
488
489        # Get the page without logging in. Should result in 302.
490        response = self.client.get("/login_protected_method_view/")
491        self.assertRedirects(
492            response, "/accounts/login/?next=/login_protected_method_view/"
493        )
494
495        # Log in
496        login = self.client.login(username="testclient", password="password")
497        self.assertTrue(login, "Could not log in")
498
499        # Request a page that requires a login
500        response = self.client.get("/login_protected_method_view/")
501        self.assertEqual(response.status_code, 200)
502        self.assertEqual(response.context["user"].username, "testclient")
503
504    def test_view_with_method_force_login(self):
505        "Request a page that is protected with a @login_required method"
506        # Get the page without logging in. Should result in 302.
507        response = self.client.get("/login_protected_method_view/")
508        self.assertRedirects(
509            response, "/accounts/login/?next=/login_protected_method_view/"
510        )
511
512        # Log in
513        self.client.force_login(self.u1)
514
515        # Request a page that requires a login
516        response = self.client.get("/login_protected_method_view/")
517        self.assertEqual(response.status_code, 200)
518        self.assertEqual(response.context["user"].username, "testclient")
519
520    def test_view_with_login_and_custom_redirect(self):
521        "Request a page that is protected with @login_required(redirect_field_name='redirect_to')"
522
523        # Get the page without logging in. Should result in 302.
524        response = self.client.get("/login_protected_view_custom_redirect/")
525        self.assertRedirects(
526            response,
527            "/accounts/login/?redirect_to=/login_protected_view_custom_redirect/",
528        )
529
530        # Log in
531        login = self.client.login(username="testclient", password="password")
532        self.assertTrue(login, "Could not log in")
533
534        # Request a page that requires a login
535        response = self.client.get("/login_protected_view_custom_redirect/")
536        self.assertEqual(response.status_code, 200)
537        self.assertEqual(response.context["user"].username, "testclient")
538
539    def test_view_with_force_login_and_custom_redirect(self):
540        """
541        Request a page that is protected with
542        @login_required(redirect_field_name='redirect_to')
543        """
544        # Get the page without logging in. Should result in 302.
545        response = self.client.get("/login_protected_view_custom_redirect/")
546        self.assertRedirects(
547            response,
548            "/accounts/login/?redirect_to=/login_protected_view_custom_redirect/",
549        )
550
551        # Log in
552        self.client.force_login(self.u1)
553
554        # Request a page that requires a login
555        response = self.client.get("/login_protected_view_custom_redirect/")
556        self.assertEqual(response.status_code, 200)
557        self.assertEqual(response.context["user"].username, "testclient")
558
559    def test_view_with_bad_login(self):
560        "Request a page that is protected with @login, but use bad credentials"
561
562        login = self.client.login(username="otheruser", password="nopassword")
563        self.assertFalse(login)
564
565    def test_view_with_inactive_login(self):
566        """
567        An inactive user may login if the authenticate backend allows it.
568        """
569        credentials = {"username": "inactive", "password": "password"}
570        self.assertFalse(self.client.login(**credentials))
571
572        with self.settings(
573            AUTHENTICATION_BACKENDS=[
574                "django.contrib.auth.backends.AllowAllUsersModelBackend"
575            ]
576        ):
577            self.assertTrue(self.client.login(**credentials))
578
579    @override_settings(
580        AUTHENTICATION_BACKENDS=[
581            "django.contrib.auth.backends.ModelBackend",
582            "django.contrib.auth.backends.AllowAllUsersModelBackend",
583        ]
584    )
585    def test_view_with_inactive_force_login(self):
586        "Request a page that is protected with @login, but use an inactive login"
587
588        # Get the page without logging in. Should result in 302.
589        response = self.client.get("/login_protected_view/")
590        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
591
592        # Log in
593        self.client.force_login(
594            self.u2, backend="django.contrib.auth.backends.AllowAllUsersModelBackend"
595        )
596
597        # Request a page that requires a login
598        response = self.client.get("/login_protected_view/")
599        self.assertEqual(response.status_code, 200)
600        self.assertEqual(response.context["user"].username, "inactive")
601
602    def test_logout(self):
603        "Request a logout after logging in"
604        # Log in
605        self.client.login(username="testclient", password="password")
606
607        # Request a page that requires a login
608        response = self.client.get("/login_protected_view/")
609        self.assertEqual(response.status_code, 200)
610        self.assertEqual(response.context["user"].username, "testclient")
611
612        # Log out
613        self.client.logout()
614
615        # Request a page that requires a login
616        response = self.client.get("/login_protected_view/")
617        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
618
619    def test_logout_with_force_login(self):
620        "Request a logout after logging in"
621        # Log in
622        self.client.force_login(self.u1)
623
624        # Request a page that requires a login
625        response = self.client.get("/login_protected_view/")
626        self.assertEqual(response.status_code, 200)
627        self.assertEqual(response.context["user"].username, "testclient")
628
629        # Log out
630        self.client.logout()
631
632        # Request a page that requires a login
633        response = self.client.get("/login_protected_view/")
634        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
635
636    @override_settings(
637        AUTHENTICATION_BACKENDS=[
638            "django.contrib.auth.backends.ModelBackend",
639            "test_client.auth_backends.TestClientBackend",
640        ],
641    )
642    def test_force_login_with_backend(self):
643        """
644        Request a page that is protected with @login_required when using
645        force_login() and passing a backend.
646        """
647        # Get the page without logging in. Should result in 302.
648        response = self.client.get("/login_protected_view/")
649        self.assertRedirects(response, "/accounts/login/?next=/login_protected_view/")
650
651        # Log in
652        self.client.force_login(
653            self.u1, backend="test_client.auth_backends.TestClientBackend"
654        )
655        self.assertEqual(self.u1.backend, "test_client.auth_backends.TestClientBackend")
656
657        # Request a page that requires a login
658        response = self.client.get("/login_protected_view/")
659        self.assertEqual(response.status_code, 200)
660        self.assertEqual(response.context["user"].username, "testclient")
661
662    @override_settings(
663        AUTHENTICATION_BACKENDS=[
664            "django.contrib.auth.backends.ModelBackend",
665            "test_client.auth_backends.TestClientBackend",
666        ],
667    )
668    def test_force_login_without_backend(self):
669        """
670        force_login() without passing a backend and with multiple backends
671        configured should automatically use the first backend.
672        """
673        self.client.force_login(self.u1)
674        response = self.client.get("/login_protected_view/")
675        self.assertEqual(response.status_code, 200)
676        self.assertEqual(response.context["user"].username, "testclient")
677        self.assertEqual(self.u1.backend, "django.contrib.auth.backends.ModelBackend")
678
679    @override_settings(
680        AUTHENTICATION_BACKENDS=[
681            "test_client.auth_backends.BackendWithoutGetUserMethod",
682            "django.contrib.auth.backends.ModelBackend",
683        ]
684    )
685    def test_force_login_with_backend_missing_get_user(self):
686        """
687        force_login() skips auth backends without a get_user() method.
688        """
689        self.client.force_login(self.u1)
690        self.assertEqual(self.u1.backend, "django.contrib.auth.backends.ModelBackend")
691
692    @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies")
693    def test_logout_cookie_sessions(self):
694        self.test_logout()
695
696    def test_view_with_permissions(self):
697        "Request a page that is protected with @permission_required"
698
699        # Get the page without logging in. Should result in 302.
700        response = self.client.get("/permission_protected_view/")
701        self.assertRedirects(
702            response, "/accounts/login/?next=/permission_protected_view/"
703        )
704
705        # Log in
706        login = self.client.login(username="testclient", password="password")
707        self.assertTrue(login, "Could not log in")
708
709        # Log in with wrong permissions. Should result in 302.
710        response = self.client.get("/permission_protected_view/")
711        self.assertRedirects(
712            response, "/accounts/login/?next=/permission_protected_view/"
713        )
714
715        # TODO: Log in with right permissions and request the page again
716
717    def test_view_with_permissions_exception(self):
718        "Request a page that is protected with @permission_required but raises an exception"
719
720        # Get the page without logging in. Should result in 403.
721        response = self.client.get("/permission_protected_view_exception/")
722        self.assertEqual(response.status_code, 403)
723
724        # Log in
725        login = self.client.login(username="testclient", password="password")
726        self.assertTrue(login, "Could not log in")
727
728        # Log in with wrong permissions. Should result in 403.
729        response = self.client.get("/permission_protected_view_exception/")
730        self.assertEqual(response.status_code, 403)
731
732    def test_view_with_method_permissions(self):
733        "Request a page that is protected with a @permission_required method"
734
735        # Get the page without logging in. Should result in 302.
736        response = self.client.get("/permission_protected_method_view/")
737        self.assertRedirects(
738            response, "/accounts/login/?next=/permission_protected_method_view/"
739        )
740
741        # Log in
742        login = self.client.login(username="testclient", password="password")
743        self.assertTrue(login, "Could not log in")
744
745        # Log in with wrong permissions. Should result in 302.
746        response = self.client.get("/permission_protected_method_view/")
747        self.assertRedirects(
748            response, "/accounts/login/?next=/permission_protected_method_view/"
749        )
750
751        # TODO: Log in with right permissions and request the page again
752
753    def test_external_redirect(self):
754        response = self.client.get("/django_project_redirect/")
755        self.assertRedirects(
756            response, "https://www.djangoproject.com/", fetch_redirect_response=False
757        )
758
759    def test_external_redirect_with_fetch_error_msg(self):
760        """
761        assertRedirects without fetch_redirect_response=False raises
762        a relevant ValueError rather than a non-descript AssertionError.
763        """
764        response = self.client.get("/django_project_redirect/")
765        msg = (
766            "The test client is unable to fetch remote URLs (got "
767            "https://www.djangoproject.com/). If the host is served by Django, "
768            "add 'www.djangoproject.com' to ALLOWED_HOSTS. "
769            "Otherwise, use assertRedirects(..., fetch_redirect_response=False)."
770        )
771        with self.assertRaisesMessage(ValueError, msg):
772            self.assertRedirects(response, "https://www.djangoproject.com/")
773
774    def test_session_modifying_view(self):
775        "Request a page that modifies the session"
776        # Session value isn't set initially
777        with self.assertRaises(KeyError):
778            self.client.session["tobacconist"]
779
780        self.client.post("/session_view/")
781        # The session was modified
782        self.assertEqual(self.client.session["tobacconist"], "hovercraft")
783
784    @override_settings(
785        INSTALLED_APPS=[], SESSION_ENGINE="django.contrib.sessions.backends.file",
786    )
787    def test_sessions_app_is_not_installed(self):
788        self.test_session_modifying_view()
789
790    @override_settings(
791        INSTALLED_APPS=[],
792        SESSION_ENGINE="django.contrib.sessions.backends.nonexistent",
793    )
794    def test_session_engine_is_invalid(self):
795        with self.assertRaisesMessage(ImportError, "nonexistent"):
796            self.test_session_modifying_view()
797
798    def test_view_with_exception(self):
799        "Request a page that is known to throw an error"
800        with self.assertRaises(KeyError):
801            self.client.get("/broken_view/")
802
803    def test_exc_info(self):
804        client = Client(raise_request_exception=False)
805        response = client.get("/broken_view/")
806        self.assertEqual(response.status_code, 500)
807        exc_type, exc_value, exc_traceback = response.exc_info
808        self.assertIs(exc_type, KeyError)
809        self.assertIsInstance(exc_value, KeyError)
810        self.assertEqual(str(exc_value), "'Oops! Looks like you wrote some bad code.'")
811        self.assertIsNotNone(exc_traceback)
812
813    def test_exc_info_none(self):
814        response = self.client.get("/get_view/")
815        self.assertIsNone(response.exc_info)
816
817    def test_mail_sending(self):
818        "Mail is redirected to a dummy outbox during test setup"
819        response = self.client.get("/mail_sending_view/")
820        self.assertEqual(response.status_code, 200)
821
822        self.assertEqual(len(mail.outbox), 1)
823        self.assertEqual(mail.outbox[0].subject, "Test message")
824        self.assertEqual(mail.outbox[0].body, "This is a test email")
825        self.assertEqual(mail.outbox[0].from_email, "from@example.com")
826        self.assertEqual(mail.outbox[0].to[0], "first@example.com")
827        self.assertEqual(mail.outbox[0].to[1], "second@example.com")
828
829    def test_reverse_lazy_decodes(self):
830        "reverse_lazy() works in the test client"
831        data = {"var": "data"}
832        response = self.client.get(reverse_lazy("get_view"), data)
833
834        # Check some response details
835        self.assertContains(response, "This is a test")
836
837    def test_relative_redirect(self):
838        response = self.client.get("/accounts/")
839        self.assertRedirects(response, "/accounts/login/")
840
841    def test_relative_redirect_no_trailing_slash(self):
842        response = self.client.get("/accounts/no_trailing_slash")
843        self.assertRedirects(response, "/accounts/login/")
844
845    def test_mass_mail_sending(self):
846        "Mass mail is redirected to a dummy outbox during test setup"
847        response = self.client.get("/mass_mail_sending_view/")
848        self.assertEqual(response.status_code, 200)
849
850        self.assertEqual(len(mail.outbox), 2)
851        self.assertEqual(mail.outbox[0].subject, "First Test message")
852        self.assertEqual(mail.outbox[0].body, "This is the first test email")
853        self.assertEqual(mail.outbox[0].from_email, "from@example.com")
854        self.assertEqual(mail.outbox[0].to[0], "first@example.com")
855        self.assertEqual(mail.outbox[0].to[1], "second@example.com")
856
857        self.assertEqual(mail.outbox[1].subject, "Second Test message")
858        self.assertEqual(mail.outbox[1].body, "This is the second test email")
859        self.assertEqual(mail.outbox[1].from_email, "from@example.com")
860        self.assertEqual(mail.outbox[1].to[0], "second@example.com")
861        self.assertEqual(mail.outbox[1].to[1], "third@example.com")
862
863    def test_exception_following_nested_client_request(self):
864        """
865        A nested test client request shouldn't clobber exception signals from
866        the outer client request.
867        """
868        with self.assertRaisesMessage(Exception, "exception message"):
869            self.client.get("/nesting_exception_view/")
870
871    def test_response_raises_multi_arg_exception(self):
872        """A request may raise an exception with more than one required arg."""
873        with self.assertRaises(TwoArgException) as cm:
874            self.client.get("/two_arg_exception/")
875        self.assertEqual(cm.exception.args, ("one", "two"))
876
877    def test_uploading_temp_file(self):
878        with tempfile.TemporaryFile() as test_file:
879            response = self.client.post("/upload_view/", data={"temp_file": test_file})
880        self.assertEqual(response.content, b"temp_file")
881
882    def test_uploading_named_temp_file(self):
883        test_file = tempfile.NamedTemporaryFile()
884        response = self.client.post(
885            "/upload_view/", data={"named_temp_file": test_file}
886        )
887        self.assertEqual(response.content, b"named_temp_file")
888
889
890@override_settings(
891    MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
892    ROOT_URLCONF="test_client.urls",
893)
894class CSRFEnabledClientTests(SimpleTestCase):
895    def test_csrf_enabled_client(self):
896        "A client can be instantiated with CSRF checks enabled"
897        csrf_client = Client(enforce_csrf_checks=True)
898        # The normal client allows the post
899        response = self.client.post("/post_view/", {})
900        self.assertEqual(response.status_code, 200)
901        # The CSRF-enabled client rejects it
902        response = csrf_client.post("/post_view/", {})
903        self.assertEqual(response.status_code, 403)
904
905
906class CustomTestClient(Client):
907    i_am_customized = "Yes"
908
909
910class CustomTestClientTest(SimpleTestCase):
911    client_class = CustomTestClient
912
913    def test_custom_test_client(self):
914        """A test case can specify a custom class for self.client."""
915        self.assertIs(hasattr(self.client, "i_am_customized"), True)
916
917
918def _generic_view(request):
919    return HttpResponse(status=200)
920
921
922@override_settings(ROOT_URLCONF="test_client.urls")
923class RequestFactoryTest(SimpleTestCase):
924    """Tests for the request factory."""
925
926    # A mapping between names of HTTP/1.1 methods and their test views.
927    http_methods_and_views = (
928        ("get", get_view),
929        ("post", post_view),
930        ("put", _generic_view),
931        ("patch", _generic_view),
932        ("delete", _generic_view),
933        ("head", _generic_view),
934        ("options", _generic_view),
935        ("trace", trace_view),
936    )
937    request_factory = RequestFactory()
938
939    def test_request_factory(self):
940        """The request factory implements all the HTTP/1.1 methods."""
941        for method_name, view in self.http_methods_and_views:
942            method = getattr(self.request_factory, method_name)
943            request = method("/somewhere/")
944            response = view(request)
945            self.assertEqual(response.status_code, 200)
946
947    def test_get_request_from_factory(self):
948        """
949        The request factory returns a templated response for a GET request.
950        """
951        request = self.request_factory.get("/somewhere/")
952        response = get_view(request)
953        self.assertContains(response, "This is a test")
954
955    def test_trace_request_from_factory(self):
956        """The request factory returns an echo response for a TRACE request."""
957        url_path = "/somewhere/"
958        request = self.request_factory.trace(url_path)
959        response = trace_view(request)
960        protocol = request.META["SERVER_PROTOCOL"]
961        echoed_request_line = "TRACE {} {}".format(url_path, protocol)
962        self.assertContains(response, echoed_request_line)

tests/auth_tests/test_auth_backends.py

  1from datetime import date
  2from unittest import mock
  3
  4from django.contrib.auth import (
  5    BACKEND_SESSION_KEY,
  6    SESSION_KEY,
  7    authenticate,
  8    get_user,
  9    signals,
 10)
 11from django.contrib.auth.backends import BaseBackend, ModelBackend
 12from django.contrib.auth.hashers import MD5PasswordHasher
 13from django.contrib.auth.models import AnonymousUser, Group, Permission, User
 14from django.contrib.contenttypes.models import ContentType
 15from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 16from django.http import HttpRequest
 17from django.test import (
 18    SimpleTestCase,
 19    TestCase,
 20    modify_settings,
 21    override_settings,
 22)
 23
 24from .models import (
 25    CustomPermissionsUser,
 26    CustomUser,
 27    CustomUserWithoutIsActiveField,
 28    ExtensionUser,
 29    UUIDUser,
 30)
 31
 32
 33class SimpleBackend(BaseBackend):
 34    def get_user_permissions(self, user_obj, obj=None):
 35        return ["user_perm"]
 36
 37    def get_group_permissions(self, user_obj, obj=None):
 38        return ["group_perm"]
 39
 40
 41@override_settings(
 42    AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.SimpleBackend"]
 43)
 44class BaseBackendTest(TestCase):
 45    @classmethod
 46    def setUpTestData(cls):
 47        cls.user = User.objects.create_user("test", "test@example.com", "test")
 48
 49    def test_get_user_permissions(self):
 50        self.assertEqual(self.user.get_user_permissions(), {"user_perm"})
 51
 52    def test_get_group_permissions(self):
 53        self.assertEqual(self.user.get_group_permissions(), {"group_perm"})
 54
 55    def test_get_all_permissions(self):
 56        self.assertEqual(self.user.get_all_permissions(), {"user_perm", "group_perm"})
 57
 58    def test_has_perm(self):
 59        self.assertIs(self.user.has_perm("user_perm"), True)
 60        self.assertIs(self.user.has_perm("group_perm"), True)
 61        self.assertIs(self.user.has_perm("other_perm", TestObj()), False)
 62
 63
 64class CountingMD5PasswordHasher(MD5PasswordHasher):
 65    """Hasher that counts how many times it computes a hash."""
 66
 67    calls = 0
 68
 69    def encode(self, *args, **kwargs):
 70        type(self).calls += 1
 71        return super().encode(*args, **kwargs)
 72
 73
 74class BaseModelBackendTest:
 75    """
 76    A base class for tests that need to validate the ModelBackend
 77    with different User models. Subclasses should define a class
 78    level UserModel attribute, and a create_users() method to
 79    construct two users for test purposes.
 80    """
 81
 82    backend = "django.contrib.auth.backends.ModelBackend"
 83
 84    def setUp(self):
 85        self.patched_settings = modify_settings(
 86            AUTHENTICATION_BACKENDS={"append": self.backend},
 87        )
 88        self.patched_settings.enable()
 89        self.create_users()
 90
 91    def tearDown(self):
 92        self.patched_settings.disable()
 93        # The custom_perms test messes with ContentTypes, which will
 94        # be cached; flush the cache to ensure there are no side effects
 95        # Refs #14975, #14925
 96        ContentType.objects.clear_cache()
 97
 98    def test_has_perm(self):
 99        user = self.UserModel._default_manager.get(pk=self.user.pk)
100        self.assertIs(user.has_perm("auth.test"), False)
101
102        user.is_staff = True
103        user.save()
104        self.assertIs(user.has_perm("auth.test"), False)
105
106        user.is_superuser = True
107        user.save()
108        self.assertIs(user.has_perm("auth.test"), True)
109
110        user.is_staff = True
111        user.is_superuser = True
112        user.is_active = False
113        user.save()
114        self.assertIs(user.has_perm("auth.test"), False)
115
116    def test_custom_perms(self):
117        user = self.UserModel._default_manager.get(pk=self.user.pk)
118        content_type = ContentType.objects.get_for_model(Group)
119        perm = Permission.objects.create(
120            name="test", content_type=content_type, codename="test"
121        )
122        user.user_permissions.add(perm)
123
124        # reloading user to purge the _perm_cache
125        user = self.UserModel._default_manager.get(pk=self.user.pk)
126        self.assertEqual(user.get_all_permissions(), {"auth.test"})
127        self.assertEqual(user.get_user_permissions(), {"auth.test"})
128        self.assertEqual(user.get_group_permissions(), set())
129        self.assertIs(user.has_module_perms("Group"), False)
130        self.assertIs(user.has_module_perms("auth"), True)
131
132        perm = Permission.objects.create(
133            name="test2", content_type=content_type, codename="test2"
134        )
135        user.user_permissions.add(perm)
136        perm = Permission.objects.create(
137            name="test3", content_type=content_type, codename="test3"
138        )
139        user.user_permissions.add(perm)
140        user = self.UserModel._default_manager.get(pk=self.user.pk)
141        expected_user_perms = {"auth.test2", "auth.test", "auth.test3"}
142        self.assertEqual(user.get_all_permissions(), expected_user_perms)
143        self.assertIs(user.has_perm("test"), False)
144        self.assertIs(user.has_perm("auth.test"), True)
145        self.assertIs(user.has_perms(["auth.test2", "auth.test3"]), True)
146
147        perm = Permission.objects.create(
148            name="test_group", content_type=content_type, codename="test_group"
149        )
150        group = Group.objects.create(name="test_group")
151        group.permissions.add(perm)
152        user.groups.add(group)
153        user = self.UserModel._default_manager.get(pk=self.user.pk)
154        self.assertEqual(
155            user.get_all_permissions(), {*expected_user_perms, "auth.test_group"}
156        )
157        self.assertEqual(user.get_user_permissions(), expected_user_perms)
158        self.assertEqual(user.get_group_permissions(), {"auth.test_group"})
159        self.assertIs(user.has_perms(["auth.test3", "auth.test_group"]), True)
160
161        user = AnonymousUser()
162        self.assertIs(user.has_perm("test"), False)
163        self.assertIs(user.has_perms(["auth.test2", "auth.test3"]), False)
164
165    def test_has_no_object_perm(self):
166        """Regressiontest for #12462"""
167        user = self.UserModel._default_manager.get(pk=self.user.pk)
168        content_type = ContentType.objects.get_for_model(Group)
169        perm = Permission.objects.create(
170            name="test", content_type=content_type, codename="test"
171        )
172        user.user_permissions.add(perm)
173
174        self.assertIs(user.has_perm("auth.test", "object"), False)
175        self.assertEqual(user.get_all_permissions("object"), set())
176        self.assertIs(user.has_perm("auth.test"), True)
177        self.assertEqual(user.get_all_permissions(), {"auth.test"})
178
179    def test_anonymous_has_no_permissions(self):
180        """
181        #17903 -- Anonymous users shouldn't have permissions in
182        ModelBackend.get_(all|user|group)_permissions().
183        """
184        backend = ModelBackend()
185
186        user = self.UserModel._default_manager.get(pk=self.user.pk)
187        content_type = ContentType.objects.get_for_model(Group)
188        user_perm = Permission.objects.create(
189            name="test", content_type=content_type, codename="test_user"
190        )
191        group_perm = Permission.objects.create(
192            name="test2", content_type=content_type, codename="test_group"
193        )
194        user.user_permissions.add(user_perm)
195
196        group = Group.objects.create(name="test_group")
197        user.groups.add(group)
198        group.permissions.add(group_perm)
199
200        self.assertEqual(
201            backend.get_all_permissions(user), {"auth.test_user", "auth.test_group"}
202        )
203        self.assertEqual(backend.get_user_permissions(user), {"auth.test_user"})
204        self.assertEqual(backend.get_group_permissions(user), {"auth.test_group"})
205
206        with mock.patch.object(self.UserModel, "is_anonymous", True):
207            self.assertEqual(backend.get_all_permissions(user), set())
208            self.assertEqual(backend.get_user_permissions(user), set())
209            self.assertEqual(backend.get_group_permissions(user), set())
210
211    def test_inactive_has_no_permissions(self):
212        """
213        #17903 -- Inactive users shouldn't have permissions in
214        ModelBackend.get_(all|user|group)_permissions().
215        """
216        backend = ModelBackend()
217
218        user = self.UserModel._default_manager.get(pk=self.user.pk)
219        content_type = ContentType.objects.get_for_model(Group)
220        user_perm = Permission.objects.create(
221            name="test", content_type=content_type, codename="test_user"
222        )
223        group_perm = Permission.objects.create(
224            name="test2", content_type=content_type, codename="test_group"
225        )
226        user.user_permissions.add(user_perm)
227
228        group = Group.objects.create(name="test_group")
229        user.groups.add(group)
230        group.permissions.add(group_perm)
231
232        self.assertEqual(
233            backend.get_all_permissions(user), {"auth.test_user", "auth.test_group"}
234        )
235        self.assertEqual(backend.get_user_permissions(user), {"auth.test_user"})
236        self.assertEqual(backend.get_group_permissions(user), {"auth.test_group"})
237
238        user.is_active = False
239        user.save()
240
241        self.assertEqual(backend.get_all_permissions(user), set())
242        self.assertEqual(backend.get_user_permissions(user), set())
243        self.assertEqual(backend.get_group_permissions(user), set())
244
245    def test_get_all_superuser_permissions(self):
246        """A superuser has all permissions. Refs #14795."""
247        user = self.UserModel._default_manager.get(pk=self.superuser.pk)
248        self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
249
250    @override_settings(
251        PASSWORD_HASHERS=["auth_tests.test_auth_backends.CountingMD5PasswordHasher"]
252    )
253    def test_authentication_timing(self):
254        """Hasher is run once regardless of whether the user exists. Refs #20760."""
255        # Re-set the password, because this tests overrides PASSWORD_HASHERS
256        self.user.set_password("test")
257        self.user.save()
258
259        CountingMD5PasswordHasher.calls = 0
260        username = getattr(self.user, self.UserModel.USERNAME_FIELD)
261        authenticate(username=username, password="test")
262        self.assertEqual(CountingMD5PasswordHasher.calls, 1)
263
264        CountingMD5PasswordHasher.calls = 0
265        authenticate(username="no_such_user", password="test")
266        self.assertEqual(CountingMD5PasswordHasher.calls, 1)
267
268    @override_settings(
269        PASSWORD_HASHERS=["auth_tests.test_auth_backends.CountingMD5PasswordHasher"]
270    )
271    def test_authentication_without_credentials(self):
272        CountingMD5PasswordHasher.calls = 0
273        for credentials in (
274            {},
275            {"username": getattr(self.user, self.UserModel.USERNAME_FIELD)},
276            {"password": "test"},
277        ):
278            with self.subTest(credentials=credentials):
279                with self.assertNumQueries(0):
280                    authenticate(**credentials)
281                self.assertEqual(CountingMD5PasswordHasher.calls, 0)
282
283
284class ModelBackendTest(BaseModelBackendTest, TestCase):
285    """
286    Tests for the ModelBackend using the default User model.
287    """
288
289    UserModel = User
290    user_credentials = {"username": "test", "password": "test"}
291
292    def create_users(self):
293        self.user = User.objects.create_user(
294            email="test@example.com", **self.user_credentials
295        )
296        self.superuser = User.objects.create_superuser(
297            username="test2", email="test2@example.com", password="test",
298        )
299
300    def test_authenticate_inactive(self):
301        """
302        An inactive user can't authenticate.
303        """
304        self.assertEqual(authenticate(**self.user_credentials), self.user)
305        self.user.is_active = False
306        self.user.save()
307        self.assertIsNone(authenticate(**self.user_credentials))
308
309    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithoutIsActiveField")
310    def test_authenticate_user_without_is_active_field(self):
311        """
312        A custom user without an `is_active` field is allowed to authenticate.
313        """
314        user = CustomUserWithoutIsActiveField.objects._create_user(
315            username="test", email="test@example.com", password="test",
316        )
317        self.assertEqual(authenticate(username="test", password="test"), user)
318
319
320@override_settings(AUTH_USER_MODEL="auth_tests.ExtensionUser")
321class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
322    """
323    Tests for the ModelBackend using the custom ExtensionUser model.
324
325    This isn't a perfect test, because both the User and ExtensionUser are
326    synchronized to the database, which wouldn't ordinary happen in
327    production. As a result, it doesn't catch errors caused by the non-
328    existence of the User table.
329
330    The specific problem is queries on .filter(groups__user) et al, which
331    makes an implicit assumption that the user model is called 'User'. In
332    production, the auth.User table won't exist, so the requested join
333    won't exist either; in testing, the auth.User *does* exist, and
334    so does the join. However, the join table won't contain any useful
335    data; for testing, we check that the data we expect actually does exist.
336    """
337
338    UserModel = ExtensionUser
339
340    def create_users(self):
341        self.user = ExtensionUser._default_manager.create_user(
342            username="test",
343            email="test@example.com",
344            password="test",
345            date_of_birth=date(2006, 4, 25),
346        )
347        self.superuser = ExtensionUser._default_manager.create_superuser(
348            username="test2",
349            email="test2@example.com",
350            password="test",
351            date_of_birth=date(1976, 11, 8),
352        )
353
354
355@override_settings(AUTH_USER_MODEL="auth_tests.CustomPermissionsUser")
356class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
357    """
358    Tests for the ModelBackend using the CustomPermissionsUser model.
359
360    As with the ExtensionUser test, this isn't a perfect test, because both
361    the User and CustomPermissionsUser are synchronized to the database,
362    which wouldn't ordinary happen in production.
363    """
364
365    UserModel = CustomPermissionsUser
366
367    def create_users(self):
368        self.user = CustomPermissionsUser._default_manager.create_user(
369            email="test@example.com", password="test", date_of_birth=date(2006, 4, 25)
370        )
371        self.superuser = CustomPermissionsUser._default_manager.create_superuser(
372            email="test2@example.com", password="test", date_of_birth=date(1976, 11, 8)
373        )
374
375
376@override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
377class CustomUserModelBackendAuthenticateTest(TestCase):
378    """
379    The model backend can accept a credentials kwarg labeled with
380    custom user model's USERNAME_FIELD.
381    """
382
383    def test_authenticate(self):
384        test_user = CustomUser._default_manager.create_user(
385            email="test@example.com", password="test", date_of_birth=date(2006, 4, 25)
386        )
387        authenticated_user = authenticate(email="test@example.com", password="test")
388        self.assertEqual(test_user, authenticated_user)
389
390
391@override_settings(AUTH_USER_MODEL="auth_tests.UUIDUser")
392class UUIDUserTests(TestCase):
393    def test_login(self):
394        """
395        A custom user with a UUID primary key should be able to login.
396        """
397        user = UUIDUser.objects.create_user(username="uuid", password="test")
398        self.assertTrue(self.client.login(username="uuid", password="test"))
399        self.assertEqual(
400            UUIDUser.objects.get(pk=self.client.session[SESSION_KEY]), user
401        )
402
403
404class TestObj:
405    pass
406
407
408class SimpleRowlevelBackend:
409    def has_perm(self, user, perm, obj=None):
410        if not obj:
411            return  # We only support row level perms
412
413        if isinstance(obj, TestObj):
414            if user.username == "test2":
415                return True
416            elif user.is_anonymous and perm == "anon":
417                return True
418            elif not user.is_active and perm == "inactive":
419                return True
420        return False
421
422    def has_module_perms(self, user, app_label):
423        return (user.is_anonymous or user.is_active) and app_label == "app1"
424
425    def get_all_permissions(self, user, obj=None):
426        if not obj:
427            return []  # We only support row level perms
428
429        if not isinstance(obj, TestObj):
430            return ["none"]
431
432        if user.is_anonymous:
433            return ["anon"]
434        if user.username == "test2":
435            return ["simple", "advanced"]
436        else:
437            return ["simple"]
438
439    def get_group_permissions(self, user, obj=None):
440        if not obj:
441            return  # We only support row level perms
442
443        if not isinstance(obj, TestObj):
444            return ["none"]
445
446        if "test_group" in [group.name for group in user.groups.all()]:
447            return ["group_perm"]
448        else:
449            return ["none"]
450
451
452@modify_settings(
453    AUTHENTICATION_BACKENDS={
454        "append": "auth_tests.test_auth_backends.SimpleRowlevelBackend",
455    }
456)
457class RowlevelBackendTest(TestCase):
458    """
459    Tests for auth backend that supports object level permissions
460    """
461
462    @classmethod
463    def setUpTestData(cls):
464        cls.user1 = User.objects.create_user("test", "test@example.com", "test")
465        cls.user2 = User.objects.create_user("test2", "test2@example.com", "test")
466        cls.user3 = User.objects.create_user("test3", "test3@example.com", "test")
467
468    def tearDown(self):
469        # The get_group_permissions test messes with ContentTypes, which will
470        # be cached; flush the cache to ensure there are no side effects
471        # Refs #14975, #14925
472        ContentType.objects.clear_cache()
473
474    def test_has_perm(self):
475        self.assertIs(self.user1.has_perm("perm", TestObj()), False)
476        self.assertIs(self.user2.has_perm("perm", TestObj()), True)
477        self.assertIs(self.user2.has_perm("perm"), False)
478        self.assertIs(self.user2.has_perms(["simple", "advanced"], TestObj()), True)
479        self.assertIs(self.user3.has_perm("perm", TestObj()), False)
480        self.assertIs(self.user3.has_perm("anon", TestObj()), False)
481        self.assertIs(self.user3.has_perms(["simple", "advanced"], TestObj()), False)
482
483    def test_get_all_permissions(self):
484        self.assertEqual(self.user1.get_all_permissions(TestObj()), {"simple"})
485        self.assertEqual(
486            self.user2.get_all_permissions(TestObj()), {"simple", "advanced"}
487        )
488        self.assertEqual(self.user2.get_all_permissions(), set())
489
490    def test_get_group_permissions(self):
491        group = Group.objects.create(name="test_group")
492        self.user3.groups.add(group)
493        self.assertEqual(self.user3.get_group_permissions(TestObj()), {"group_perm"})
494
495
496@override_settings(
497    AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.SimpleRowlevelBackend"],
498)
499class AnonymousUserBackendTest(SimpleTestCase):
500    """
501    Tests for AnonymousUser delegating to backend.
502    """
503
504    def setUp(self):
505        self.user1 = AnonymousUser()
506
507    def test_has_perm(self):
508        self.assertIs(self.user1.has_perm("perm", TestObj()), False)
509        self.assertIs(self.user1.has_perm("anon", TestObj()), True)
510
511    def test_has_perms(self):
512        self.assertIs(self.user1.has_perms(["anon"], TestObj()), True)
513        self.assertIs(self.user1.has_perms(["anon", "perm"], TestObj()), False)
514
515    def test_has_module_perms(self):
516        self.assertIs(self.user1.has_module_perms("app1"), True)
517        self.assertIs(self.user1.has_module_perms("app2"), False)
518
519    def test_get_all_permissions(self):
520        self.assertEqual(self.user1.get_all_permissions(TestObj()), {"anon"})
521
522
523@override_settings(AUTHENTICATION_BACKENDS=[])
524class NoBackendsTest(TestCase):
525    """
526    An appropriate error is raised if no auth backends are provided.
527    """
528
529    @classmethod
530    def setUpTestData(cls):
531        cls.user = User.objects.create_user("test", "test@example.com", "test")
532
533    def test_raises_exception(self):
534        msg = (
535            "No authentication backends have been defined. "
536            "Does AUTHENTICATION_BACKENDS contain anything?"
537        )
538        with self.assertRaisesMessage(ImproperlyConfigured, msg):
539            self.user.has_perm(("perm", TestObj()))
540
541
542@override_settings(
543    AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.SimpleRowlevelBackend"]
544)
545class InActiveUserBackendTest(TestCase):
546    """
547    Tests for an inactive user
548    """
549
550    @classmethod
551    def setUpTestData(cls):
552        cls.user1 = User.objects.create_user("test", "test@example.com", "test")
553        cls.user1.is_active = False
554        cls.user1.save()
555
556    def test_has_perm(self):
557        self.assertIs(self.user1.has_perm("perm", TestObj()), False)
558        self.assertIs(self.user1.has_perm("inactive", TestObj()), True)
559
560    def test_has_module_perms(self):
561        self.assertIs(self.user1.has_module_perms("app1"), False)
562        self.assertIs(self.user1.has_module_perms("app2"), False)
563
564
565class PermissionDeniedBackend:
566    """
567    Always raises PermissionDenied in `authenticate`, `has_perm` and `has_module_perms`.
568    """
569
570    def authenticate(self, request, username=None, password=None):
571        raise PermissionDenied
572
573    def has_perm(self, user_obj, perm, obj=None):
574        raise PermissionDenied
575
576    def has_module_perms(self, user_obj, app_label):
577        raise PermissionDenied
578
579
580class PermissionDeniedBackendTest(TestCase):
581    """
582    Other backends are not checked once a backend raises PermissionDenied
583    """
584
585    backend = "auth_tests.test_auth_backends.PermissionDeniedBackend"
586
587    @classmethod
588    def setUpTestData(cls):
589        cls.user1 = User.objects.create_user("test", "test@example.com", "test")
590
591    def setUp(self):
592        self.user_login_failed = []
593        signals.user_login_failed.connect(self.user_login_failed_listener)
594
595    def tearDown(self):
596        signals.user_login_failed.disconnect(self.user_login_failed_listener)
597
598    def user_login_failed_listener(self, sender, credentials, **kwargs):
599        self.user_login_failed.append(credentials)
600
601    @modify_settings(AUTHENTICATION_BACKENDS={"prepend": backend})
602    def test_permission_denied(self):
603        "user is not authenticated after a backend raises permission denied #2550"
604        self.assertIsNone(authenticate(username="test", password="test"))
605        # user_login_failed signal is sent.
606        self.assertEqual(
607            self.user_login_failed,
608            [{"password": "********************", "username": "test"}],
609        )
610
611    @modify_settings(AUTHENTICATION_BACKENDS={"append": backend})
612    def test_authenticates(self):
613        self.assertEqual(authenticate(username="test", password="test"), self.user1)
614
615    @modify_settings(AUTHENTICATION_BACKENDS={"prepend": backend})
616    def test_has_perm_denied(self):
617        content_type = ContentType.objects.get_for_model(Group)
618        perm = Permission.objects.create(
619            name="test", content_type=content_type, codename="test"
620        )
621        self.user1.user_permissions.add(perm)
622
623        self.assertIs(self.user1.has_perm("auth.test"), False)
624        self.assertIs(self.user1.has_module_perms("auth"), False)
625
626    @modify_settings(AUTHENTICATION_BACKENDS={"append": backend})
627    def test_has_perm(self):
628        content_type = ContentType.objects.get_for_model(Group)
629        perm = Permission.objects.create(
630            name="test", content_type=content_type, codename="test"
631        )
632        self.user1.user_permissions.add(perm)
633
634        self.assertIs(self.user1.has_perm("auth.test"), True)
635        self.assertIs(self.user1.has_module_perms("auth"), True)
636
637
638class NewModelBackend(ModelBackend):
639    pass
640
641
642class ChangedBackendSettingsTest(TestCase):
643    """
644    Tests for changes in the settings.AUTHENTICATION_BACKENDS
645    """
646
647    backend = "auth_tests.test_auth_backends.NewModelBackend"
648
649    TEST_USERNAME = "test_user"
650    TEST_PASSWORD = "test_password"
651    TEST_EMAIL = "test@example.com"
652
653    @classmethod
654    def setUpTestData(cls):
655        User.objects.create_user(cls.TEST_USERNAME, cls.TEST_EMAIL, cls.TEST_PASSWORD)
656
657    @override_settings(AUTHENTICATION_BACKENDS=[backend])
658    def test_changed_backend_settings(self):
659        """
660        Removing a backend configured in AUTHENTICATION_BACKENDS makes already
661        logged-in users disconnect.
662        """
663        # Get a session for the test user
664        self.assertTrue(
665            self.client.login(username=self.TEST_USERNAME, password=self.TEST_PASSWORD,)
666        )
667        # Prepare a request object
668        request = HttpRequest()
669        request.session = self.client.session
670        # Remove NewModelBackend
671        with self.settings(
672            AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.ModelBackend"]
673        ):
674            # Get the user from the request
675            user = get_user(request)
676
677            # Assert that the user retrieval is successful and the user is
678            # anonymous as the backend is not longer available.
679            self.assertIsNotNone(user)
680            self.assertTrue(user.is_anonymous)
681
682
683class TypeErrorBackend:
684    """
685    Always raises TypeError.
686    """
687
688    def authenticate(self, request, username=None, password=None):
689        raise TypeError
690
691
692class SkippedBackend:
693    def authenticate(self):
694        # Doesn't accept any credentials so is skipped by authenticate().
695        pass
696
697
698class AuthenticateTests(TestCase):
699    @classmethod
700    def setUpTestData(cls):
701        cls.user1 = User.objects.create_user("test", "test@example.com", "test")
702
703    @override_settings(
704        AUTHENTICATION_BACKENDS=["auth_tests.test_auth_backends.TypeErrorBackend"]
705    )
706    def test_type_error_raised(self):
707        """A TypeError within a backend is propagated properly (#18171)."""
708        with self.assertRaises(TypeError):
709            authenticate(username="test", password="test")
710
711    @override_settings(
712        AUTHENTICATION_BACKENDS=(
713            "auth_tests.test_auth_backends.SkippedBackend",
714            "django.contrib.auth.backends.ModelBackend",
715        )
716    )
717    def test_skips_backends_without_arguments(self):
718        """
719        A backend (SkippedBackend) is ignored if it doesn't accept the
720        credentials as arguments.
721        """
722        self.assertEqual(authenticate(username="test", password="test"), self.user1)
723
724
725class ImproperlyConfiguredUserModelTest(TestCase):
726    """
727    An exception from within get_user_model() is propagated and doesn't
728    raise an UnboundLocalError (#21439).
729    """
730
731    @classmethod
732    def setUpTestData(cls):
733        cls.user1 = User.objects.create_user("test", "test@example.com", "test")
734
735    def setUp(self):
736        self.client.login(username="test", password="test")
737
738    @override_settings(AUTH_USER_MODEL="thismodel.doesntexist")
739    def test_does_not_shadow_exception(self):
740        # Prepare a request object
741        request = HttpRequest()
742        request.session = self.client.session
743
744        msg = (
745            "AUTH_USER_MODEL refers to model 'thismodel.doesntexist' "
746            "that has not been installed"
747        )
748        with self.assertRaisesMessage(ImproperlyConfigured, msg):
749            get_user(request)
750
751
752class ImportedModelBackend(ModelBackend):
753    pass
754
755
756class CustomModelBackend(ModelBackend):
757    pass
758
759
760class OtherModelBackend(ModelBackend):
761    pass
762
763
764class ImportedBackendTests(TestCase):
765    """
766    #23925 - The backend path added to the session should be the same
767    as the one defined in AUTHENTICATION_BACKENDS setting.
768    """
769
770    backend = "auth_tests.backend_alias.ImportedModelBackend"
771
772    @override_settings(AUTHENTICATION_BACKENDS=[backend])
773    def test_backend_path(self):
774        username = "username"
775        password = "password"
776        User.objects.create_user(username, "email", password)
777        self.assertTrue(self.client.login(username=username, password=password))
778        request = HttpRequest()
779        request.session = self.client.session
780        self.assertEqual(request.session[BACKEND_SESSION_KEY], self.backend)
781
782
783class SelectingBackendTests(TestCase):
784    backend = "auth_tests.test_auth_backends.CustomModelBackend"
785    other_backend = "auth_tests.test_auth_backends.OtherModelBackend"
786    username = "username"
787    password = "password"
788
789    def assertBackendInSession(self, backend):
790        request = HttpRequest()
791        request.session = self.client.session
792        self.assertEqual(request.session[BACKEND_SESSION_KEY], backend)
793
794    @override_settings(AUTHENTICATION_BACKENDS=[backend])
795    def test_backend_path_login_without_authenticate_single_backend(self):
796        user = User.objects.create_user(self.username, "email", self.password)
797        self.client._login(user)
798        self.assertBackendInSession(self.backend)
799
800    @override_settings(AUTHENTICATION_BACKENDS=[backend, other_backend])
801    def test_backend_path_login_without_authenticate_multiple_backends(self):
802        user = User.objects.create_user(self.username, "email", self.password)
803        expected_message = (
804            "You have multiple authentication backends configured and "
805            "therefore must provide the `backend` argument or set the "
806            "`backend` attribute on the user."
807        )
808        with self.assertRaisesMessage(ValueError, expected_message):
809            self.client._login(user)
810
811    def test_non_string_backend(self):
812        user = User.objects.create_user(self.username, "email", self.password)
813        expected_message = (
814            "backend must be a dotted import path string (got "
815            "<class 'django.contrib.auth.backends.ModelBackend'>)."
816        )
817        with self.assertRaisesMessage(TypeError, expected_message):
818            self.client._login(user, backend=ModelBackend)
819
820    @override_settings(AUTHENTICATION_BACKENDS=[backend, other_backend])
821    def test_backend_path_login_with_explicit_backends(self):
822        user = User.objects.create_user(self.username, "email", self.password)
823        self.client._login(user, self.other_backend)
824        self.assertBackendInSession(self.other_backend)
825
826
827@override_settings(
828    AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.AllowAllUsersModelBackend"]
829)
830class AllowAllUsersModelBackendTest(TestCase):
831    """
832    Inactive users may authenticate with the AllowAllUsersModelBackend.
833    """
834
835    user_credentials = {"username": "test", "password": "test"}
836
837    @classmethod
838    def setUpTestData(cls):
839        cls.user = User.objects.create_user(
840            email="test@example.com", is_active=False, **cls.user_credentials
841        )
842
843    def test_authenticate(self):
844        self.assertFalse(self.user.is_active)
845        self.assertEqual(authenticate(**self.user_credentials), self.user)
846
847    def test_get_user(self):
848        self.client.force_login(self.user)
849        request = HttpRequest()
850        request.session = self.client.session
851        user = get_user(request)
852        self.assertEqual(user, self.user)

tests/admin_views/urls.py

 1from django.urls import include, path
 2
 3from . import admin, custom_has_permission_admin, customadmin, views
 4from .test_autocomplete_view import site as autocomplete_site
 5
 6urlpatterns = [
 7    path("test_admin/admin/doc/", include("django.contrib.admindocs.urls")),
 8    path("test_admin/admin/secure-view/", views.secure_view, name="secure_view"),
 9    path("test_admin/admin/secure-view2/", views.secure_view2, name="secure_view2"),
10    path("test_admin/admin/", admin.site.urls),
11    path("test_admin/admin2/", customadmin.site.urls),
12    path(
13        "test_admin/admin3/",
14        (admin.site.get_urls(), "admin", "admin3"),
15        {"form_url": "pony"},
16    ),
17    path("test_admin/admin4/", customadmin.simple_site.urls),
18    path("test_admin/admin5/", admin.site2.urls),
19    path("test_admin/admin6/", admin.site6.urls),
20    path("test_admin/admin7/", admin.site7.urls),
21    # All admin views accept `extra_context` to allow adding it like this:
22    path(
23        "test_admin/admin8/",
24        (admin.site.get_urls(), "admin", "admin-extra-context"),
25        {"extra_context": {}},
26    ),
27    path("test_admin/admin9/", admin.site9.urls),
28    path("test_admin/has_permission_admin/", custom_has_permission_admin.site.urls),
29    path("test_admin/autocomplete_admin/", autocomplete_site.urls),
30]

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/auth_tests/test_views.py

   1import datetime
   2import itertools
   3import os
   4import re
   5from importlib import import_module
   6from unittest import mock
   7from urllib.parse import quote
   8
   9from django.apps import apps
  10from django.conf import settings
  11from django.contrib.admin.models import LogEntry
  12from django.contrib.auth import (
  13    BACKEND_SESSION_KEY,
  14    REDIRECT_FIELD_NAME,
  15    SESSION_KEY,
  16)
  17from django.contrib.auth.forms import (
  18    AuthenticationForm,
  19    PasswordChangeForm,
  20    SetPasswordForm,
  21)
  22from django.contrib.auth.models import Permission, User
  23from django.contrib.auth.views import (
  24    INTERNAL_RESET_SESSION_TOKEN,
  25    LoginView,
  26    logout_then_login,
  27    redirect_to_login,
  28)
  29from django.contrib.contenttypes.models import ContentType
  30from django.contrib.sessions.middleware import SessionMiddleware
  31from django.contrib.sites.requests import RequestSite
  32from django.core import mail
  33from django.db import connection
  34from django.http import HttpRequest
  35from django.middleware.csrf import CsrfViewMiddleware, get_token
  36from django.test import Client, TestCase, override_settings
  37from django.test.client import RedirectCycleError
  38from django.urls import NoReverseMatch, reverse, reverse_lazy
  39from django.utils.http import urlsafe_base64_encode
  40
  41from .client import PasswordResetConfirmClient
  42from .models import CustomUser, UUIDUser
  43from .settings import AUTH_TEMPLATES
  44
  45
  46@override_settings(
  47    LANGUAGES=[("en", "English")],
  48    LANGUAGE_CODE="en",
  49    TEMPLATES=AUTH_TEMPLATES,
  50    ROOT_URLCONF="auth_tests.urls",
  51)
  52class AuthViewsTestCase(TestCase):
  53    """
  54    Helper base class for all the follow test cases.
  55    """
  56
  57    @classmethod
  58    def setUpTestData(cls):
  59        cls.u1 = User.objects.create_user(
  60            username="testclient", password="password", email="testclient@example.com"
  61        )
  62        cls.u3 = User.objects.create_user(
  63            username="staff", password="password", email="staffmember@example.com"
  64        )
  65
  66    def login(self, username="testclient", password="password"):
  67        response = self.client.post(
  68            "/login/", {"username": username, "password": password,}
  69        )
  70        self.assertIn(SESSION_KEY, self.client.session)
  71        return response
  72
  73    def logout(self):
  74        response = self.client.get("/admin/logout/")
  75        self.assertEqual(response.status_code, 200)
  76        self.assertNotIn(SESSION_KEY, self.client.session)
  77
  78    def assertFormError(self, response, error):
  79        """Assert that error is found in response.context['form'] errors"""
  80        form_errors = list(itertools.chain(*response.context["form"].errors.values()))
  81        self.assertIn(str(error), form_errors)
  82
  83
  84@override_settings(ROOT_URLCONF="django.contrib.auth.urls")
  85class AuthViewNamedURLTests(AuthViewsTestCase):
  86    def test_named_urls(self):
  87        "Named URLs should be reversible"
  88        expected_named_urls = [
  89            ("login", [], {}),
  90            ("logout", [], {}),
  91            ("password_change", [], {}),
  92            ("password_change_done", [], {}),
  93            ("password_reset", [], {}),
  94            ("password_reset_done", [], {}),
  95            (
  96                "password_reset_confirm",
  97                [],
  98                {"uidb64": "aaaaaaa", "token": "1111-aaaaa",},
  99            ),
 100            ("password_reset_complete", [], {}),
 101        ]
 102        for name, args, kwargs in expected_named_urls:
 103            with self.subTest(name=name):
 104                try:
 105                    reverse(name, args=args, kwargs=kwargs)
 106                except NoReverseMatch:
 107                    self.fail(
 108                        "Reversal of url named '%s' failed with NoReverseMatch" % name
 109                    )
 110
 111
 112class PasswordResetTest(AuthViewsTestCase):
 113    def setUp(self):
 114        self.client = PasswordResetConfirmClient()
 115
 116    def test_email_not_found(self):
 117        """If the provided email is not registered, don't raise any error but
 118        also don't send any email."""
 119        response = self.client.get("/password_reset/")
 120        self.assertEqual(response.status_code, 200)
 121        response = self.client.post(
 122            "/password_reset/", {"email": "not_a_real_email@email.com"}
 123        )
 124        self.assertEqual(response.status_code, 302)
 125        self.assertEqual(len(mail.outbox), 0)
 126
 127    def test_email_found(self):
 128        "Email is sent if a valid email address is provided for password reset"
 129        response = self.client.post(
 130            "/password_reset/", {"email": "staffmember@example.com"}
 131        )
 132        self.assertEqual(response.status_code, 302)
 133        self.assertEqual(len(mail.outbox), 1)
 134        self.assertIn("http://", mail.outbox[0].body)
 135        self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
 136        # optional multipart text/html email has been added.  Make sure original,
 137        # default functionality is 100% the same
 138        self.assertFalse(mail.outbox[0].message().is_multipart())
 139
 140    def test_extra_email_context(self):
 141        """
 142        extra_email_context should be available in the email template context.
 143        """
 144        response = self.client.post(
 145            "/password_reset_extra_email_context/",
 146            {"email": "staffmember@example.com"},
 147        )
 148        self.assertEqual(response.status_code, 302)
 149        self.assertEqual(len(mail.outbox), 1)
 150        self.assertIn('Email email context: "Hello!"', mail.outbox[0].body)
 151        self.assertIn("http://custom.example.com/reset/", mail.outbox[0].body)
 152
 153    def test_html_mail_template(self):
 154        """
 155        A multipart email with text/plain and text/html is sent
 156        if the html_email_template parameter is passed to the view
 157        """
 158        response = self.client.post(
 159            "/password_reset/html_email_template/", {"email": "staffmember@example.com"}
 160        )
 161        self.assertEqual(response.status_code, 302)
 162        self.assertEqual(len(mail.outbox), 1)
 163        message = mail.outbox[0].message()
 164        self.assertEqual(len(message.get_payload()), 2)
 165        self.assertTrue(message.is_multipart())
 166        self.assertEqual(message.get_payload(0).get_content_type(), "text/plain")
 167        self.assertEqual(message.get_payload(1).get_content_type(), "text/html")
 168        self.assertNotIn("<html>", message.get_payload(0).get_payload())
 169        self.assertIn("<html>", message.get_payload(1).get_payload())
 170
 171    def test_email_found_custom_from(self):
 172        "Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
 173        response = self.client.post(
 174            "/password_reset_from_email/", {"email": "staffmember@example.com"}
 175        )
 176        self.assertEqual(response.status_code, 302)
 177        self.assertEqual(len(mail.outbox), 1)
 178        self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
 179
 180    # Skip any 500 handler action (like sending more mail...)
 181    @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
 182    def test_poisoned_http_host(self):
 183        "Poisoned HTTP_HOST headers can't be used for reset emails"
 184        # This attack is based on the way browsers handle URLs. The colon
 185        # should be used to separate the port, but if the URL contains an @,
 186        # the colon is interpreted as part of a username for login purposes,
 187        # making 'evil.com' the request domain. Since HTTP_HOST is used to
 188        # produce a meaningful reset URL, we need to be certain that the
 189        # HTTP_HOST header isn't poisoned. This is done as a check when get_host()
 190        # is invoked, but we check here as a practical consequence.
 191        with self.assertLogs("django.security.DisallowedHost", "ERROR"):
 192            response = self.client.post(
 193                "/password_reset/",
 194                {"email": "staffmember@example.com"},
 195                HTTP_HOST="www.example:dr.frankenstein@evil.tld",
 196            )
 197        self.assertEqual(response.status_code, 400)
 198        self.assertEqual(len(mail.outbox), 0)
 199
 200    # Skip any 500 handler action (like sending more mail...)
 201    @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
 202    def test_poisoned_http_host_admin_site(self):
 203        "Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
 204        with self.assertLogs("django.security.DisallowedHost", "ERROR"):
 205            response = self.client.post(
 206                "/admin_password_reset/",
 207                {"email": "staffmember@example.com"},
 208                HTTP_HOST="www.example:dr.frankenstein@evil.tld",
 209            )
 210        self.assertEqual(response.status_code, 400)
 211        self.assertEqual(len(mail.outbox), 0)
 212
 213    def _test_confirm_start(self):
 214        # Start by creating the email
 215        self.client.post("/password_reset/", {"email": "staffmember@example.com"})
 216        self.assertEqual(len(mail.outbox), 1)
 217        return self._read_signup_email(mail.outbox[0])
 218
 219    def _read_signup_email(self, email):
 220        urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
 221        self.assertIsNotNone(urlmatch, "No URL found in sent email")
 222        return urlmatch.group(), urlmatch.groups()[0]
 223
 224    def test_confirm_valid(self):
 225        url, path = self._test_confirm_start()
 226        response = self.client.get(path)
 227        # redirect to a 'complete' page:
 228        self.assertContains(response, "Please enter your new password")
 229
 230    def test_confirm_invalid(self):
 231        url, path = self._test_confirm_start()
 232        # Let's munge the token in the path, but keep the same length,
 233        # in case the URLconf will reject a different length.
 234        path = path[:-5] + ("0" * 4) + path[-1]
 235
 236        response = self.client.get(path)
 237        self.assertContains(response, "The password reset link was invalid")
 238
 239    def test_confirm_invalid_user(self):
 240        # A nonexistent user returns a 200 response, not a 404.
 241        response = self.client.get("/reset/123456/1-1/")
 242        self.assertContains(response, "The password reset link was invalid")
 243
 244    def test_confirm_overflow_user(self):
 245        # A base36 user id that overflows int returns a 200 response.
 246        response = self.client.get("/reset/zzzzzzzzzzzzz/1-1/")
 247        self.assertContains(response, "The password reset link was invalid")
 248
 249    def test_confirm_invalid_post(self):
 250        # Same as test_confirm_invalid, but trying to do a POST instead.
 251        url, path = self._test_confirm_start()
 252        path = path[:-5] + ("0" * 4) + path[-1]
 253
 254        self.client.post(
 255            path, {"new_password1": "anewpassword", "new_password2": " anewpassword",}
 256        )
 257        # Check the password has not been changed
 258        u = User.objects.get(email="staffmember@example.com")
 259        self.assertTrue(not u.check_password("anewpassword"))
 260
 261    def test_confirm_invalid_hash(self):
 262        """A POST with an invalid token is rejected."""
 263        u = User.objects.get(email="staffmember@example.com")
 264        original_password = u.password
 265        url, path = self._test_confirm_start()
 266        path_parts = path.split("-")
 267        path_parts[-1] = ("0") * 20 + "/"
 268        path = "-".join(path_parts)
 269
 270        response = self.client.post(
 271            path, {"new_password1": "anewpassword", "new_password2": "anewpassword",}
 272        )
 273        self.assertIs(response.context["validlink"], False)
 274        u.refresh_from_db()
 275        self.assertEqual(original_password, u.password)  # password hasn't changed
 276
 277    def test_confirm_complete(self):
 278        url, path = self._test_confirm_start()
 279        response = self.client.post(
 280            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 281        )
 282        # Check the password has been changed
 283        u = User.objects.get(email="staffmember@example.com")
 284        self.assertTrue(u.check_password("anewpassword"))
 285        # The reset token is deleted from the session.
 286        self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session)
 287
 288        # Check we can't use the link again
 289        response = self.client.get(path)
 290        self.assertContains(response, "The password reset link was invalid")
 291
 292    def test_confirm_different_passwords(self):
 293        url, path = self._test_confirm_start()
 294        response = self.client.post(
 295            path, {"new_password1": "anewpassword", "new_password2": "x"}
 296        )
 297        self.assertFormError(
 298            response, SetPasswordForm.error_messages["password_mismatch"]
 299        )
 300
 301    def test_reset_redirect_default(self):
 302        response = self.client.post(
 303            "/password_reset/", {"email": "staffmember@example.com"}
 304        )
 305        self.assertRedirects(
 306            response, "/password_reset/done/", fetch_redirect_response=False
 307        )
 308
 309    def test_reset_custom_redirect(self):
 310        response = self.client.post(
 311            "/password_reset/custom_redirect/", {"email": "staffmember@example.com"}
 312        )
 313        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
 314
 315    def test_reset_custom_redirect_named(self):
 316        response = self.client.post(
 317            "/password_reset/custom_redirect/named/",
 318            {"email": "staffmember@example.com"},
 319        )
 320        self.assertRedirects(
 321            response, "/password_reset/", fetch_redirect_response=False
 322        )
 323
 324    def test_confirm_redirect_default(self):
 325        url, path = self._test_confirm_start()
 326        response = self.client.post(
 327            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 328        )
 329        self.assertRedirects(response, "/reset/done/", fetch_redirect_response=False)
 330
 331    def test_confirm_redirect_custom(self):
 332        url, path = self._test_confirm_start()
 333        path = path.replace("/reset/", "/reset/custom/")
 334        response = self.client.post(
 335            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 336        )
 337        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
 338
 339    def test_confirm_redirect_custom_named(self):
 340        url, path = self._test_confirm_start()
 341        path = path.replace("/reset/", "/reset/custom/named/")
 342        response = self.client.post(
 343            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 344        )
 345        self.assertRedirects(
 346            response, "/password_reset/", fetch_redirect_response=False
 347        )
 348
 349    def test_confirm_custom_reset_url_token(self):
 350        url, path = self._test_confirm_start()
 351        path = path.replace("/reset/", "/reset/custom/token/")
 352        self.client.reset_url_token = "set-passwordcustom"
 353        response = self.client.post(
 354            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"},
 355        )
 356        self.assertRedirects(response, "/reset/done/", fetch_redirect_response=False)
 357
 358    def test_confirm_login_post_reset(self):
 359        url, path = self._test_confirm_start()
 360        path = path.replace("/reset/", "/reset/post_reset_login/")
 361        response = self.client.post(
 362            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 363        )
 364        self.assertRedirects(response, "/reset/done/", fetch_redirect_response=False)
 365        self.assertIn(SESSION_KEY, self.client.session)
 366
 367    @override_settings(
 368        AUTHENTICATION_BACKENDS=[
 369            "django.contrib.auth.backends.ModelBackend",
 370            "django.contrib.auth.backends.AllowAllUsersModelBackend",
 371        ]
 372    )
 373    def test_confirm_login_post_reset_custom_backend(self):
 374        # This backend is specified in the URL pattern.
 375        backend = "django.contrib.auth.backends.AllowAllUsersModelBackend"
 376        url, path = self._test_confirm_start()
 377        path = path.replace("/reset/", "/reset/post_reset_login_custom_backend/")
 378        response = self.client.post(
 379            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 380        )
 381        self.assertRedirects(response, "/reset/done/", fetch_redirect_response=False)
 382        self.assertIn(SESSION_KEY, self.client.session)
 383        self.assertEqual(self.client.session[BACKEND_SESSION_KEY], backend)
 384
 385    def test_confirm_login_post_reset_already_logged_in(self):
 386        url, path = self._test_confirm_start()
 387        path = path.replace("/reset/", "/reset/post_reset_login/")
 388        self.login()
 389        response = self.client.post(
 390            path, {"new_password1": "anewpassword", "new_password2": "anewpassword"}
 391        )
 392        self.assertRedirects(response, "/reset/done/", fetch_redirect_response=False)
 393        self.assertIn(SESSION_KEY, self.client.session)
 394
 395    def test_confirm_display_user_from_form(self):
 396        url, path = self._test_confirm_start()
 397        response = self.client.get(path)
 398        # The password_reset_confirm() view passes the user object to the
 399        # SetPasswordForm``, even on GET requests (#16919). For this test,
 400        # {{ form.user }}`` is rendered in the template
 401        # registration/password_reset_confirm.html.
 402        username = User.objects.get(email="staffmember@example.com").username
 403        self.assertContains(response, "Hello, %s." % username)
 404        # However, the view should NOT pass any user object on a form if the
 405        # password reset link was invalid.
 406        response = self.client.get("/reset/zzzzzzzzzzzzz/1-1/")
 407        self.assertContains(response, "Hello, .")
 408
 409    def test_confirm_link_redirects_to_set_password_page(self):
 410        url, path = self._test_confirm_start()
 411        # Don't use PasswordResetConfirmClient (self.client) here which
 412        # automatically fetches the redirect page.
 413        client = Client()
 414        response = client.get(path)
 415        token = response.resolver_match.kwargs["token"]
 416        uuidb64 = response.resolver_match.kwargs["uidb64"]
 417        self.assertRedirects(response, "/reset/%s/set-password/" % uuidb64)
 418        self.assertEqual(client.session["_password_reset_token"], token)
 419
 420    def test_confirm_custom_reset_url_token_link_redirects_to_set_password_page(self):
 421        url, path = self._test_confirm_start()
 422        path = path.replace("/reset/", "/reset/custom/token/")
 423        client = Client()
 424        response = client.get(path)
 425        token = response.resolver_match.kwargs["token"]
 426        uuidb64 = response.resolver_match.kwargs["uidb64"]
 427        self.assertRedirects(
 428            response, "/reset/custom/token/%s/set-passwordcustom/" % uuidb64
 429        )
 430        self.assertEqual(client.session["_password_reset_token"], token)
 431
 432    def test_invalid_link_if_going_directly_to_the_final_reset_password_url(self):
 433        url, path = self._test_confirm_start()
 434        _, uuidb64, _ = path.strip("/").split("/")
 435        response = Client().get("/reset/%s/set-password/" % uuidb64)
 436        self.assertContains(response, "The password reset link was invalid")
 437
 438
 439@override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
 440class CustomUserPasswordResetTest(AuthViewsTestCase):
 441    user_email = "staffmember@example.com"
 442
 443    @classmethod
 444    def setUpTestData(cls):
 445        cls.u1 = CustomUser.custom_objects.create(
 446            email="staffmember@example.com", date_of_birth=datetime.date(1976, 11, 8),
 447        )
 448        cls.u1.set_password("password")
 449        cls.u1.save()
 450
 451    def setUp(self):
 452        self.client = PasswordResetConfirmClient()
 453
 454    def _test_confirm_start(self):
 455        # Start by creating the email
 456        response = self.client.post("/password_reset/", {"email": self.user_email})
 457        self.assertEqual(response.status_code, 302)
 458        self.assertEqual(len(mail.outbox), 1)
 459        return self._read_signup_email(mail.outbox[0])
 460
 461    def _read_signup_email(self, email):
 462        urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body)
 463        self.assertIsNotNone(urlmatch, "No URL found in sent email")
 464        return urlmatch.group(), urlmatch.groups()[0]
 465
 466    def test_confirm_valid_custom_user(self):
 467        url, path = self._test_confirm_start()
 468        response = self.client.get(path)
 469        # redirect to a 'complete' page:
 470        self.assertContains(response, "Please enter your new password")
 471        # then submit a new password
 472        response = self.client.post(
 473            path, {"new_password1": "anewpassword", "new_password2": "anewpassword",}
 474        )
 475        self.assertRedirects(response, "/reset/done/")
 476
 477
 478@override_settings(AUTH_USER_MODEL="auth_tests.UUIDUser")
 479class UUIDUserPasswordResetTest(CustomUserPasswordResetTest):
 480    def _test_confirm_start(self):
 481        # instead of fixture
 482        UUIDUser.objects.create_user(
 483            email=self.user_email, username="foo", password="foo",
 484        )
 485        return super()._test_confirm_start()
 486
 487    def test_confirm_invalid_uuid(self):
 488        """A uidb64 that decodes to a non-UUID doesn't crash."""
 489        _, path = self._test_confirm_start()
 490        invalid_uidb64 = urlsafe_base64_encode(b"INVALID_UUID")
 491        first, _uuidb64_, second = path.strip("/").split("/")
 492        response = self.client.get(
 493            "/" + "/".join((first, invalid_uidb64, second)) + "/"
 494        )
 495        self.assertContains(response, "The password reset link was invalid")
 496
 497
 498class ChangePasswordTest(AuthViewsTestCase):
 499    def fail_login(self):
 500        response = self.client.post(
 501            "/login/", {"username": "testclient", "password": "password",}
 502        )
 503        self.assertFormError(
 504            response,
 505            AuthenticationForm.error_messages["invalid_login"]
 506            % {"username": User._meta.get_field("username").verbose_name},
 507        )
 508
 509    def logout(self):
 510        self.client.get("/logout/")
 511
 512    def test_password_change_fails_with_invalid_old_password(self):
 513        self.login()
 514        response = self.client.post(
 515            "/password_change/",
 516            {
 517                "old_password": "donuts",
 518                "new_password1": "password1",
 519                "new_password2": "password1",
 520            },
 521        )
 522        self.assertFormError(
 523            response, PasswordChangeForm.error_messages["password_incorrect"]
 524        )
 525
 526    def test_password_change_fails_with_mismatched_passwords(self):
 527        self.login()
 528        response = self.client.post(
 529            "/password_change/",
 530            {
 531                "old_password": "password",
 532                "new_password1": "password1",
 533                "new_password2": "donuts",
 534            },
 535        )
 536        self.assertFormError(
 537            response, SetPasswordForm.error_messages["password_mismatch"]
 538        )
 539
 540    def test_password_change_succeeds(self):
 541        self.login()
 542        self.client.post(
 543            "/password_change/",
 544            {
 545                "old_password": "password",
 546                "new_password1": "password1",
 547                "new_password2": "password1",
 548            },
 549        )
 550        self.fail_login()
 551        self.login(password="password1")
 552
 553    def test_password_change_done_succeeds(self):
 554        self.login()
 555        response = self.client.post(
 556            "/password_change/",
 557            {
 558                "old_password": "password",
 559                "new_password1": "password1",
 560                "new_password2": "password1",
 561            },
 562        )
 563        self.assertRedirects(
 564            response, "/password_change/done/", fetch_redirect_response=False
 565        )
 566
 567    @override_settings(LOGIN_URL="/login/")
 568    def test_password_change_done_fails(self):
 569        response = self.client.get("/password_change/done/")
 570        self.assertRedirects(
 571            response,
 572            "/login/?next=/password_change/done/",
 573            fetch_redirect_response=False,
 574        )
 575
 576    def test_password_change_redirect_default(self):
 577        self.login()
 578        response = self.client.post(
 579            "/password_change/",
 580            {
 581                "old_password": "password",
 582                "new_password1": "password1",
 583                "new_password2": "password1",
 584            },
 585        )
 586        self.assertRedirects(
 587            response, "/password_change/done/", fetch_redirect_response=False
 588        )
 589
 590    def test_password_change_redirect_custom(self):
 591        self.login()
 592        response = self.client.post(
 593            "/password_change/custom/",
 594            {
 595                "old_password": "password",
 596                "new_password1": "password1",
 597                "new_password2": "password1",
 598            },
 599        )
 600        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
 601
 602    def test_password_change_redirect_custom_named(self):
 603        self.login()
 604        response = self.client.post(
 605            "/password_change/custom/named/",
 606            {
 607                "old_password": "password",
 608                "new_password1": "password1",
 609                "new_password2": "password1",
 610            },
 611        )
 612        self.assertRedirects(
 613            response, "/password_reset/", fetch_redirect_response=False
 614        )
 615
 616
 617class SessionAuthenticationTests(AuthViewsTestCase):
 618    def test_user_password_change_updates_session(self):
 619        """
 620        #21649 - Ensure contrib.auth.views.password_change updates the user's
 621        session auth hash after a password change so the session isn't logged out.
 622        """
 623        self.login()
 624        original_session_key = self.client.session.session_key
 625        response = self.client.post(
 626            "/password_change/",
 627            {
 628                "old_password": "password",
 629                "new_password1": "password1",
 630                "new_password2": "password1",
 631            },
 632        )
 633        # if the hash isn't updated, retrieving the redirection page will fail.
 634        self.assertRedirects(response, "/password_change/done/")
 635        # The session key is rotated.
 636        self.assertNotEqual(original_session_key, self.client.session.session_key)
 637
 638
 639class LoginTest(AuthViewsTestCase):
 640    def test_current_site_in_context_after_login(self):
 641        response = self.client.get(reverse("login"))
 642        self.assertEqual(response.status_code, 200)
 643        if apps.is_installed("django.contrib.sites"):
 644            Site = apps.get_model("sites.Site")
 645            site = Site.objects.get_current()
 646            self.assertEqual(response.context["site"], site)
 647            self.assertEqual(response.context["site_name"], site.name)
 648        else:
 649            self.assertIsInstance(response.context["site"], RequestSite)
 650        self.assertIsInstance(response.context["form"], AuthenticationForm)
 651
 652    def test_security_check(self):
 653        login_url = reverse("login")
 654
 655        # These URLs should not pass the security check.
 656        bad_urls = (
 657            "http://example.com",
 658            "http:///example.com",
 659            "https://example.com",
 660            "ftp://example.com",
 661            "///example.com",
 662            "//example.com",
 663            'javascript:alert("XSS")',
 664        )
 665        for bad_url in bad_urls:
 666            with self.subTest(bad_url=bad_url):
 667                nasty_url = "%(url)s?%(next)s=%(bad_url)s" % {
 668                    "url": login_url,
 669                    "next": REDIRECT_FIELD_NAME,
 670                    "bad_url": quote(bad_url),
 671                }
 672                response = self.client.post(
 673                    nasty_url, {"username": "testclient", "password": "password",}
 674                )
 675                self.assertEqual(response.status_code, 302)
 676                self.assertNotIn(
 677                    bad_url, response.url, "%s should be blocked" % bad_url
 678                )
 679
 680        # These URLs should pass the security check.
 681        good_urls = (
 682            "/view/?param=http://example.com",
 683            "/view/?param=https://example.com",
 684            "/view?param=ftp://example.com",
 685            "view/?param=//example.com",
 686            "https://testserver/",
 687            "HTTPS://testserver/",
 688            "//testserver/",
 689            "/url%20with%20spaces/",
 690        )
 691        for good_url in good_urls:
 692            with self.subTest(good_url=good_url):
 693                safe_url = "%(url)s?%(next)s=%(good_url)s" % {
 694                    "url": login_url,
 695                    "next": REDIRECT_FIELD_NAME,
 696                    "good_url": quote(good_url),
 697                }
 698                response = self.client.post(
 699                    safe_url, {"username": "testclient", "password": "password",}
 700                )
 701                self.assertEqual(response.status_code, 302)
 702                self.assertIn(good_url, response.url, "%s should be allowed" % good_url)
 703
 704    def test_security_check_https(self):
 705        login_url = reverse("login")
 706        non_https_next_url = "http://testserver/path"
 707        not_secured_url = "%(url)s?%(next)s=%(next_url)s" % {
 708            "url": login_url,
 709            "next": REDIRECT_FIELD_NAME,
 710            "next_url": quote(non_https_next_url),
 711        }
 712        post_data = {
 713            "username": "testclient",
 714            "password": "password",
 715        }
 716        response = self.client.post(not_secured_url, post_data, secure=True)
 717        self.assertEqual(response.status_code, 302)
 718        self.assertNotEqual(response.url, non_https_next_url)
 719        self.assertEqual(response.url, settings.LOGIN_REDIRECT_URL)
 720
 721    def test_login_form_contains_request(self):
 722        # The custom authentication form for this login requires a request to
 723        # initialize it.
 724        response = self.client.post(
 725            "/custom_request_auth_login/",
 726            {"username": "testclient", "password": "password",},
 727        )
 728        # The login was successful.
 729        self.assertRedirects(
 730            response, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
 731        )
 732
 733    def test_login_csrf_rotate(self):
 734        """
 735        Makes sure that a login rotates the currently-used CSRF token.
 736        """
 737        # Do a GET to establish a CSRF token
 738        # The test client isn't used here as it's a test for middleware.
 739        req = HttpRequest()
 740        CsrfViewMiddleware().process_view(req, LoginView.as_view(), (), {})
 741        # get_token() triggers CSRF token inclusion in the response
 742        get_token(req)
 743        resp = LoginView.as_view()(req)
 744        resp2 = CsrfViewMiddleware().process_response(req, resp)
 745        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
 746        token1 = csrf_cookie.coded_value
 747
 748        # Prepare the POST request
 749        req = HttpRequest()
 750        req.COOKIES[settings.CSRF_COOKIE_NAME] = token1
 751        req.method = "POST"
 752        req.POST = {
 753            "username": "testclient",
 754            "password": "password",
 755            "csrfmiddlewaretoken": token1,
 756        }
 757
 758        # Use POST request to log in
 759        SessionMiddleware().process_request(req)
 760        CsrfViewMiddleware().process_view(req, LoginView.as_view(), (), {})
 761        req.META[
 762            "SERVER_NAME"
 763        ] = "testserver"  # Required to have redirect work in login view
 764        req.META["SERVER_PORT"] = 80
 765        resp = LoginView.as_view()(req)
 766        resp2 = CsrfViewMiddleware().process_response(req, resp)
 767        csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
 768        token2 = csrf_cookie.coded_value
 769
 770        # Check the CSRF token switched
 771        self.assertNotEqual(token1, token2)
 772
 773    def test_session_key_flushed_on_login(self):
 774        """
 775        To avoid reusing another user's session, ensure a new, empty session is
 776        created if the existing session corresponds to a different authenticated
 777        user.
 778        """
 779        self.login()
 780        original_session_key = self.client.session.session_key
 781
 782        self.login(username="staff")
 783        self.assertNotEqual(original_session_key, self.client.session.session_key)
 784
 785    def test_session_key_flushed_on_login_after_password_change(self):
 786        """
 787        As above, but same user logging in after a password change.
 788        """
 789        self.login()
 790        original_session_key = self.client.session.session_key
 791
 792        # If no password change, session key should not be flushed.
 793        self.login()
 794        self.assertEqual(original_session_key, self.client.session.session_key)
 795
 796        user = User.objects.get(username="testclient")
 797        user.set_password("foobar")
 798        user.save()
 799
 800        self.login(password="foobar")
 801        self.assertNotEqual(original_session_key, self.client.session.session_key)
 802
 803    def test_login_session_without_hash_session_key(self):
 804        """
 805        Session without django.contrib.auth.HASH_SESSION_KEY should login
 806        without an exception.
 807        """
 808        user = User.objects.get(username="testclient")
 809        engine = import_module(settings.SESSION_ENGINE)
 810        session = engine.SessionStore()
 811        session[SESSION_KEY] = user.id
 812        session.save()
 813        original_session_key = session.session_key
 814        self.client.cookies[settings.SESSION_COOKIE_NAME] = original_session_key
 815
 816        self.login()
 817        self.assertNotEqual(original_session_key, self.client.session.session_key)
 818
 819
 820class LoginURLSettings(AuthViewsTestCase):
 821    """Tests for settings.LOGIN_URL."""
 822
 823    def assertLoginURLEquals(self, url):
 824        response = self.client.get("/login_required/")
 825        self.assertRedirects(response, url, fetch_redirect_response=False)
 826
 827    @override_settings(LOGIN_URL="/login/")
 828    def test_standard_login_url(self):
 829        self.assertLoginURLEquals("/login/?next=/login_required/")
 830
 831    @override_settings(LOGIN_URL="login")
 832    def test_named_login_url(self):
 833        self.assertLoginURLEquals("/login/?next=/login_required/")
 834
 835    @override_settings(LOGIN_URL="http://remote.example.com/login")
 836    def test_remote_login_url(self):
 837        quoted_next = quote("http://testserver/login_required/")
 838        expected = "http://remote.example.com/login?next=%s" % quoted_next
 839        self.assertLoginURLEquals(expected)
 840
 841    @override_settings(LOGIN_URL="https:///login/")
 842    def test_https_login_url(self):
 843        quoted_next = quote("http://testserver/login_required/")
 844        expected = "https:///login/?next=%s" % quoted_next
 845        self.assertLoginURLEquals(expected)
 846
 847    @override_settings(LOGIN_URL="/login/?pretty=1")
 848    def test_login_url_with_querystring(self):
 849        self.assertLoginURLEquals("/login/?pretty=1&next=/login_required/")
 850
 851    @override_settings(LOGIN_URL="http://remote.example.com/login/?next=/default/")
 852    def test_remote_login_url_with_next_querystring(self):
 853        quoted_next = quote("http://testserver/login_required/")
 854        expected = "http://remote.example.com/login/?next=%s" % quoted_next
 855        self.assertLoginURLEquals(expected)
 856
 857    @override_settings(LOGIN_URL=reverse_lazy("login"))
 858    def test_lazy_login_url(self):
 859        self.assertLoginURLEquals("/login/?next=/login_required/")
 860
 861
 862class LoginRedirectUrlTest(AuthViewsTestCase):
 863    """Tests for settings.LOGIN_REDIRECT_URL."""
 864
 865    def assertLoginRedirectURLEqual(self, url):
 866        response = self.login()
 867        self.assertRedirects(response, url, fetch_redirect_response=False)
 868
 869    def test_default(self):
 870        self.assertLoginRedirectURLEqual("/accounts/profile/")
 871
 872    @override_settings(LOGIN_REDIRECT_URL="/custom/")
 873    def test_custom(self):
 874        self.assertLoginRedirectURLEqual("/custom/")
 875
 876    @override_settings(LOGIN_REDIRECT_URL="password_reset")
 877    def test_named(self):
 878        self.assertLoginRedirectURLEqual("/password_reset/")
 879
 880    @override_settings(LOGIN_REDIRECT_URL="http://remote.example.com/welcome/")
 881    def test_remote(self):
 882        self.assertLoginRedirectURLEqual("http://remote.example.com/welcome/")
 883
 884
 885class RedirectToLoginTests(AuthViewsTestCase):
 886    """Tests for the redirect_to_login view"""
 887
 888    @override_settings(LOGIN_URL=reverse_lazy("login"))
 889    def test_redirect_to_login_with_lazy(self):
 890        login_redirect_response = redirect_to_login(next="/else/where/")
 891        expected = "/login/?next=/else/where/"
 892        self.assertEqual(expected, login_redirect_response.url)
 893
 894    @override_settings(LOGIN_URL=reverse_lazy("login"))
 895    def test_redirect_to_login_with_lazy_and_unicode(self):
 896        login_redirect_response = redirect_to_login(next="/else/where/झ/")
 897        expected = "/login/?next=/else/where/%E0%A4%9D/"
 898        self.assertEqual(expected, login_redirect_response.url)
 899
 900
 901class LogoutThenLoginTests(AuthViewsTestCase):
 902    """Tests for the logout_then_login view"""
 903
 904    def confirm_logged_out(self):
 905        self.assertNotIn(SESSION_KEY, self.client.session)
 906
 907    @override_settings(LOGIN_URL="/login/")
 908    def test_default_logout_then_login(self):
 909        self.login()
 910        req = HttpRequest()
 911        req.method = "GET"
 912        req.session = self.client.session
 913        response = logout_then_login(req)
 914        self.confirm_logged_out()
 915        self.assertRedirects(response, "/login/", fetch_redirect_response=False)
 916
 917    def test_logout_then_login_with_custom_login(self):
 918        self.login()
 919        req = HttpRequest()
 920        req.method = "GET"
 921        req.session = self.client.session
 922        response = logout_then_login(req, login_url="/custom/")
 923        self.confirm_logged_out()
 924        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
 925
 926
 927class LoginRedirectAuthenticatedUser(AuthViewsTestCase):
 928    dont_redirect_url = "/login/redirect_authenticated_user_default/"
 929    do_redirect_url = "/login/redirect_authenticated_user/"
 930
 931    def test_default(self):
 932        """Stay on the login page by default."""
 933        self.login()
 934        response = self.client.get(self.dont_redirect_url)
 935        self.assertEqual(response.status_code, 200)
 936        self.assertEqual(response.context["next"], "")
 937
 938    def test_guest(self):
 939        """If not logged in, stay on the same page."""
 940        response = self.client.get(self.do_redirect_url)
 941        self.assertEqual(response.status_code, 200)
 942
 943    def test_redirect(self):
 944        """If logged in, go to default redirected URL."""
 945        self.login()
 946        response = self.client.get(self.do_redirect_url)
 947        self.assertRedirects(
 948            response, "/accounts/profile/", fetch_redirect_response=False
 949        )
 950
 951    @override_settings(LOGIN_REDIRECT_URL="/custom/")
 952    def test_redirect_url(self):
 953        """If logged in, go to custom redirected URL."""
 954        self.login()
 955        response = self.client.get(self.do_redirect_url)
 956        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
 957
 958    def test_redirect_param(self):
 959        """If next is specified as a GET parameter, go there."""
 960        self.login()
 961        url = self.do_redirect_url + "?next=/custom_next/"
 962        response = self.client.get(url)
 963        self.assertRedirects(response, "/custom_next/", fetch_redirect_response=False)
 964
 965    def test_redirect_loop(self):
 966        """
 967        Detect a redirect loop if LOGIN_REDIRECT_URL is not correctly set,
 968        with and without custom parameters.
 969        """
 970        self.login()
 971        msg = (
 972            "Redirection loop for authenticated user detected. Check that "
 973            "your LOGIN_REDIRECT_URL doesn't point to a login page."
 974        )
 975        with self.settings(LOGIN_REDIRECT_URL=self.do_redirect_url):
 976            with self.assertRaisesMessage(ValueError, msg):
 977                self.client.get(self.do_redirect_url)
 978
 979            url = self.do_redirect_url + "?bla=2"
 980            with self.assertRaisesMessage(ValueError, msg):
 981                self.client.get(url)
 982
 983    def test_permission_required_not_logged_in(self):
 984        # Not logged in ...
 985        with self.settings(LOGIN_URL=self.do_redirect_url):
 986            # redirected to login.
 987            response = self.client.get("/permission_required_redirect/", follow=True)
 988            self.assertEqual(response.status_code, 200)
 989            # exception raised.
 990            response = self.client.get("/permission_required_exception/", follow=True)
 991            self.assertEqual(response.status_code, 403)
 992            # redirected to login.
 993            response = self.client.get(
 994                "/login_and_permission_required_exception/", follow=True
 995            )
 996            self.assertEqual(response.status_code, 200)
 997
 998    def test_permission_required_logged_in(self):
 999        self.login()
1000        # Already logged in...
1001        with self.settings(LOGIN_URL=self.do_redirect_url):
1002            # redirect loop encountered.
1003            with self.assertRaisesMessage(
1004                RedirectCycleError, "Redirect loop detected."
1005            ):
1006                self.client.get("/permission_required_redirect/", follow=True)
1007            # exception raised.
1008            response = self.client.get("/permission_required_exception/", follow=True)
1009            self.assertEqual(response.status_code, 403)
1010            # exception raised.
1011            response = self.client.get(
1012                "/login_and_permission_required_exception/", follow=True
1013            )
1014            self.assertEqual(response.status_code, 403)
1015
1016
1017class LoginSuccessURLAllowedHostsTest(AuthViewsTestCase):
1018    def test_success_url_allowed_hosts_same_host(self):
1019        response = self.client.post(
1020            "/login/allowed_hosts/",
1021            {
1022                "username": "testclient",
1023                "password": "password",
1024                "next": "https://testserver/home",
1025            },
1026        )
1027        self.assertIn(SESSION_KEY, self.client.session)
1028        self.assertRedirects(
1029            response, "https://testserver/home", fetch_redirect_response=False
1030        )
1031
1032    def test_success_url_allowed_hosts_safe_host(self):
1033        response = self.client.post(
1034            "/login/allowed_hosts/",
1035            {
1036                "username": "testclient",
1037                "password": "password",
1038                "next": "https://otherserver/home",
1039            },
1040        )
1041        self.assertIn(SESSION_KEY, self.client.session)
1042        self.assertRedirects(
1043            response, "https://otherserver/home", fetch_redirect_response=False
1044        )
1045
1046    def test_success_url_allowed_hosts_unsafe_host(self):
1047        response = self.client.post(
1048            "/login/allowed_hosts/",
1049            {
1050                "username": "testclient",
1051                "password": "password",
1052                "next": "https://evil/home",
1053            },
1054        )
1055        self.assertIn(SESSION_KEY, self.client.session)
1056        self.assertRedirects(
1057            response, "/accounts/profile/", fetch_redirect_response=False
1058        )
1059
1060
1061class LogoutTest(AuthViewsTestCase):
1062    def confirm_logged_out(self):
1063        self.assertNotIn(SESSION_KEY, self.client.session)
1064
1065    def test_logout_default(self):
1066        "Logout without next_page option renders the default template"
1067        self.login()
1068        response = self.client.get("/logout/")
1069        self.assertContains(response, "Logged out")
1070        self.confirm_logged_out()
1071
1072    def test_logout_with_post(self):
1073        self.login()
1074        response = self.client.post("/logout/")
1075        self.assertContains(response, "Logged out")
1076        self.confirm_logged_out()
1077
1078    def test_14377(self):
1079        # Bug 14377
1080        self.login()
1081        response = self.client.get("/logout/")
1082        self.assertIn("site", response.context)
1083
1084    def test_logout_doesnt_cache(self):
1085        """
1086        The logout() view should send "no-cache" headers for reasons described
1087        in #25490.
1088        """
1089        response = self.client.get("/logout/")
1090        self.assertIn("no-store", response["Cache-Control"])
1091
1092    def test_logout_with_overridden_redirect_url(self):
1093        # Bug 11223
1094        self.login()
1095        response = self.client.get("/logout/next_page/")
1096        self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False)
1097
1098        response = self.client.get("/logout/next_page/?next=/login/")
1099        self.assertRedirects(response, "/login/", fetch_redirect_response=False)
1100
1101        self.confirm_logged_out()
1102
1103    def test_logout_with_next_page_specified(self):
1104        "Logout with next_page option given redirects to specified resource"
1105        self.login()
1106        response = self.client.get("/logout/next_page/")
1107        self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False)
1108        self.confirm_logged_out()
1109
1110    def test_logout_with_redirect_argument(self):
1111        "Logout with query string redirects to specified resource"
1112        self.login()
1113        response = self.client.get("/logout/?next=/login/")
1114        self.assertRedirects(response, "/login/", fetch_redirect_response=False)
1115        self.confirm_logged_out()
1116
1117    def test_logout_with_custom_redirect_argument(self):
1118        "Logout with custom query string redirects to specified resource"
1119        self.login()
1120        response = self.client.get("/logout/custom_query/?follow=/somewhere/")
1121        self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False)
1122        self.confirm_logged_out()
1123
1124    def test_logout_with_named_redirect(self):
1125        "Logout resolves names or URLs passed as next_page."
1126        self.login()
1127        response = self.client.get("/logout/next_page/named/")
1128        self.assertRedirects(
1129            response, "/password_reset/", fetch_redirect_response=False
1130        )
1131        self.confirm_logged_out()
1132
1133    def test_success_url_allowed_hosts_same_host(self):
1134        self.login()
1135        response = self.client.get("/logout/allowed_hosts/?next=https://testserver/")
1136        self.assertRedirects(
1137            response, "https://testserver/", fetch_redirect_response=False
1138        )
1139        self.confirm_logged_out()
1140
1141    def test_success_url_allowed_hosts_safe_host(self):
1142        self.login()
1143        response = self.client.get("/logout/allowed_hosts/?next=https://otherserver/")
1144        self.assertRedirects(
1145            response, "https://otherserver/", fetch_redirect_response=False
1146        )
1147        self.confirm_logged_out()
1148
1149    def test_success_url_allowed_hosts_unsafe_host(self):
1150        self.login()
1151        response = self.client.get("/logout/allowed_hosts/?next=https://evil/")
1152        self.assertRedirects(
1153            response, "/logout/allowed_hosts/", fetch_redirect_response=False
1154        )
1155        self.confirm_logged_out()
1156
1157    def test_security_check(self):
1158        logout_url = reverse("logout")
1159
1160        # These URLs should not pass the security check.
1161        bad_urls = (
1162            "http://example.com",
1163            "http:///example.com",
1164            "https://example.com",
1165            "ftp://example.com",
1166            "///example.com",
1167            "//example.com",
1168            'javascript:alert("XSS")',
1169        )
1170        for bad_url in bad_urls:
1171            with self.subTest(bad_url=bad_url):
1172                nasty_url = "%(url)s?%(next)s=%(bad_url)s" % {
1173                    "url": logout_url,
1174                    "next": REDIRECT_FIELD_NAME,
1175                    "bad_url": quote(bad_url),
1176                }
1177                self.login()
1178                response = self.client.get(nasty_url)
1179                self.assertEqual(response.status_code, 302)
1180                self.assertNotIn(
1181                    bad_url, response.url, "%s should be blocked" % bad_url
1182                )
1183                self.confirm_logged_out()
1184
1185        # These URLs should pass the security check.
1186        good_urls = (
1187            "/view/?param=http://example.com",
1188            "/view/?param=https://example.com",
1189            "/view?param=ftp://example.com",
1190            "view/?param=//example.com",
1191            "https://testserver/",
1192            "HTTPS://testserver/",
1193            "//testserver/",
1194            "/url%20with%20spaces/",
1195        )
1196        for good_url in good_urls:
1197            with self.subTest(good_url=good_url):
1198                safe_url = "%(url)s?%(next)s=%(good_url)s" % {
1199                    "url": logout_url,
1200                    "next": REDIRECT_FIELD_NAME,
1201                    "good_url": quote(good_url),
1202                }
1203                self.login()
1204                response = self.client.get(safe_url)
1205                self.assertEqual(response.status_code, 302)
1206                self.assertIn(good_url, response.url, "%s should be allowed" % good_url)
1207                self.confirm_logged_out()
1208
1209    def test_security_check_https(self):
1210        logout_url = reverse("logout")
1211        non_https_next_url = "http://testserver/"
1212        url = "%(url)s?%(next)s=%(next_url)s" % {
1213            "url": logout_url,
1214            "next": REDIRECT_FIELD_NAME,
1215            "next_url": quote(non_https_next_url),
1216        }
1217        self.login()
1218        response = self.client.get(url, secure=True)
1219        self.assertRedirects(response, logout_url, fetch_redirect_response=False)
1220        self.confirm_logged_out()
1221
1222    def test_logout_preserve_language(self):
1223        """Language is preserved after logout."""
1224        self.login()
1225        self.client.post("/setlang/", {"language": "pl"})
1226        self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "pl")
1227        self.client.get("/logout/")
1228        self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "pl")
1229
1230    @override_settings(LOGOUT_REDIRECT_URL="/custom/")
1231    def test_logout_redirect_url_setting(self):
1232        self.login()
1233        response = self.client.get("/logout/")
1234        self.assertRedirects(response, "/custom/", fetch_redirect_response=False)
1235
1236    @override_settings(LOGOUT_REDIRECT_URL="logout")
1237    def test_logout_redirect_url_named_setting(self):
1238        self.login()
1239        response = self.client.get("/logout/")
1240        self.assertRedirects(response, "/logout/", fetch_redirect_response=False)
1241
1242
1243def get_perm(Model, perm):
1244    ct = ContentType.objects.get_for_model(Model)
1245    return Permission.objects.get(content_type=ct, codename=perm)
1246
1247
1248# Redirect in test_user_change_password will fail if session auth hash
1249# isn't updated after password change (#21649)
1250@override_settings(ROOT_URLCONF="auth_tests.urls_admin")
1251class ChangelistTests(AuthViewsTestCase):
1252    @classmethod
1253    def setUpTestData(cls):
1254        super().setUpTestData()
1255        # Make me a superuser before logging in.
1256        User.objects.filter(username="testclient").update(
1257            is_staff=True, is_superuser=True
1258        )
1259
1260    def setUp(self):
1261        self.login()
1262        # Get the latest last_login value.
1263        self.admin = User.objects.get(pk=self.u1.pk)
1264
1265    def get_user_data(self, user):
1266        return {
1267            "username": user.username,
1268            "password": user.password,
1269            "email": user.email,
1270            "is_active": user.is_active,
1271            "is_staff": user.is_staff,
1272            "is_superuser": user.is_superuser,
1273            "last_login_0": user.last_login.strftime("%Y-%m-%d"),
1274            "last_login_1": user.last_login.strftime("%H:%M:%S"),
1275            "initial-last_login_0": user.last_login.strftime("%Y-%m-%d"),
1276            "initial-last_login_1": user.last_login.strftime("%H:%M:%S"),
1277            "date_joined_0": user.date_joined.strftime("%Y-%m-%d"),
1278            "date_joined_1": user.date_joined.strftime("%H:%M:%S"),
1279            "initial-date_joined_0": user.date_joined.strftime("%Y-%m-%d"),
1280            "initial-date_joined_1": user.date_joined.strftime("%H:%M:%S"),
1281            "first_name": user.first_name,
1282            "last_name": user.last_name,
1283        }
1284
1285    # #20078 - users shouldn't be allowed to guess password hashes via
1286    # repeated password__startswith queries.
1287    def test_changelist_disallows_password_lookups(self):
1288        # A lookup that tries to filter on password isn't OK
1289        with self.assertLogs("django.security.DisallowedModelAdminLookup", "ERROR"):
1290            response = self.client.get(
1291                reverse("auth_test_admin:auth_user_changelist")
1292                + "?password__startswith=sha1$"
1293            )
1294        self.assertEqual(response.status_code, 400)
1295
1296    def test_user_change_email(self):
1297        data = self.get_user_data(self.admin)
1298        data["email"] = "new_" + data["email"]
1299        response = self.client.post(
1300            reverse("auth_test_admin:auth_user_change", args=(self.admin.pk,)), data
1301        )
1302        self.assertRedirects(response, reverse("auth_test_admin:auth_user_changelist"))
1303        row = LogEntry.objects.latest("id")
1304        self.assertEqual(row.get_change_message(), "Changed Email address.")
1305
1306    def test_user_not_change(self):
1307        response = self.client.post(
1308            reverse("auth_test_admin:auth_user_change", args=(self.admin.pk,)),
1309            self.get_user_data(self.admin),
1310        )
1311        self.assertRedirects(response, reverse("auth_test_admin:auth_user_changelist"))
1312        row = LogEntry.objects.latest("id")
1313        self.assertEqual(row.get_change_message(), "No fields changed.")
1314
1315    def test_user_change_password(self):
1316        user_change_url = reverse(
1317            "auth_test_admin:auth_user_change", args=(self.admin.pk,)
1318        )
1319        password_change_url = reverse(
1320            "auth_test_admin:auth_user_password_change", args=(self.admin.pk,)
1321        )
1322
1323        response = self.client.get(user_change_url)
1324        # Test the link inside password field help_text.
1325        rel_link = re.search(
1326            r'you can change the password using <a href="([^"]*)">this form</a>',
1327            response.content.decode(),
1328        ).groups()[0]
1329        self.assertEqual(
1330            os.path.normpath(user_change_url + rel_link),
1331            os.path.normpath(password_change_url),
1332        )
1333
1334        response = self.client.post(
1335            password_change_url, {"password1": "password1", "password2": "password1",}
1336        )
1337        self.assertRedirects(response, user_change_url)
1338        row = LogEntry.objects.latest("id")
1339        self.assertEqual(row.get_change_message(), "Changed password.")
1340        self.logout()
1341        self.login(password="password1")
1342
1343    def test_user_change_different_user_password(self):
1344        u = User.objects.get(email="staffmember@example.com")
1345        response = self.client.post(
1346            reverse("auth_test_admin:auth_user_password_change", args=(u.pk,)),
1347            {"password1": "password1", "password2": "password1",},
1348        )
1349        self.assertRedirects(
1350            response, reverse("auth_test_admin:auth_user_change", args=(u.pk,))
1351        )
1352        row = LogEntry.objects.latest("id")
1353        self.assertEqual(row.user_id, self.admin.pk)
1354        self.assertEqual(row.object_id, str(u.pk))
1355        self.assertEqual(row.get_change_message(), "Changed password.")
1356
1357    def test_password_change_bad_url(self):
1358        response = self.client.get(
1359            reverse("auth_test_admin:auth_user_password_change", args=("foobar",))
1360        )
1361        self.assertEqual(response.status_code, 404)
1362
1363    @mock.patch("django.contrib.auth.admin.UserAdmin.has_change_permission")
1364    def test_user_change_password_passes_user_to_has_change_permission(
1365        self, has_change_permission
1366    ):
1367        url = reverse(
1368            "auth_test_admin:auth_user_password_change", args=(self.admin.pk,)
1369        )
1370        self.client.post(url, {"password1": "password1", "password2": "password1"})
1371        (_request, user), _kwargs = has_change_permission.call_args
1372        self.assertEqual(user.pk, self.admin.pk)
1373
1374    def test_view_user_password_is_readonly(self):
1375        u = User.objects.get(username="testclient")
1376        u.is_superuser = False
1377        u.save()
1378        original_password = u.password
1379        u.user_permissions.add(get_perm(User, "view_user"))
1380        response = self.client.get(
1381            reverse("auth_test_admin:auth_user_change", args=(u.pk,)),
1382        )
1383        algo, salt, hash_string = u.password.split("$")
1384        self.assertContains(response, '<div class="readonly">testclient</div>')
1385        # ReadOnlyPasswordHashWidget is used to render the field.
1386        self.assertContains(
1387            response,
1388            "<strong>algorithm</strong>: %s\n\n"
1389            "<strong>salt</strong>: %s**********\n\n"
1390            "<strong>hash</strong>: %s**************************\n\n"
1391            % (algo, salt[:2], hash_string[:6],),
1392            html=True,
1393        )
1394        # Value in POST data is ignored.
1395        data = self.get_user_data(u)
1396        data["password"] = "shouldnotchange"
1397        change_url = reverse("auth_test_admin:auth_user_change", args=(u.pk,))
1398        response = self.client.post(change_url, data)
1399        self.assertEqual(response.status_code, 403)
1400        u.refresh_from_db()
1401        self.assertEqual(u.password, original_password)
1402
1403
1404@override_settings(
1405    AUTH_USER_MODEL="auth_tests.UUIDUser",
1406    ROOT_URLCONF="auth_tests.urls_custom_user_admin",
1407)
1408class UUIDUserTests(TestCase):
1409    def test_admin_password_change(self):
1410        u = UUIDUser.objects.create_superuser(
1411            username="uuid", email="foo@bar.com", password="test"
1412        )
1413        self.assertTrue(self.client.login(username="uuid", password="test"))
1414
1415        user_change_url = reverse(
1416            "custom_user_admin:auth_tests_uuiduser_change", args=(u.pk,)
1417        )
1418        response = self.client.get(user_change_url)
1419        self.assertEqual(response.status_code, 200)
1420
1421        password_change_url = reverse(
1422            "custom_user_admin:auth_user_password_change", args=(u.pk,)
1423        )
1424        response = self.client.get(password_change_url)
1425        self.assertEqual(response.status_code, 200)
1426
1427        # A LogEntry is created with pk=1 which breaks a FK constraint on MySQL
1428        with connection.constraint_checks_disabled():
1429            response = self.client.post(
1430                password_change_url,
1431                {"password1": "password1", "password2": "password1",},
1432            )
1433        self.assertRedirects(response, user_change_url)
1434        row = LogEntry.objects.latest("id")
1435        self.assertEqual(row.user_id, 1)  # hardcoded in CustomUserAdmin.log_change()
1436        self.assertEqual(row.object_id, str(u.pk))
1437        self.assertEqual(row.get_change_message(), "Changed password.")
1438
1439        # The LogEntry.user column isn't altered to a UUID type so it's set to
1440        # an integer manually in CustomUserAdmin to avoid an error. To avoid a
1441        # constraint error, delete the entry before constraints are checked
1442        # after the test.
1443        row.delete()

tests/auth_tests/test_forms.py

   1import datetime
   2import re
   3from unittest import mock
   4
   5from django import forms
   6from django.contrib.auth.forms import (
   7    AdminPasswordChangeForm,
   8    AuthenticationForm,
   9    PasswordChangeForm,
  10    PasswordResetForm,
  11    ReadOnlyPasswordHashField,
  12    ReadOnlyPasswordHashWidget,
  13    SetPasswordForm,
  14    UserChangeForm,
  15    UserCreationForm,
  16)
  17from django.contrib.auth.models import User
  18from django.contrib.auth.signals import user_login_failed
  19from django.contrib.sites.models import Site
  20from django.core import mail
  21from django.core.mail import EmailMultiAlternatives
  22from django.forms.fields import CharField, Field, IntegerField
  23from django.test import SimpleTestCase, TestCase, override_settings
  24from django.utils import translation
  25from django.utils.text import capfirst
  26from django.utils.translation import gettext as _
  27
  28from .models.custom_user import (
  29    CustomUser,
  30    CustomUserWithoutIsActiveField,
  31    ExtensionUser,
  32)
  33from .models.with_custom_email_field import CustomEmailField
  34from .models.with_integer_username import IntegerUsernameUser
  35from .settings import AUTH_TEMPLATES
  36
  37
  38class TestDataMixin:
  39    @classmethod
  40    def setUpTestData(cls):
  41        cls.u1 = User.objects.create_user(
  42            username="testclient", password="password", email="testclient@example.com"
  43        )
  44        cls.u2 = User.objects.create_user(
  45            username="inactive", password="password", is_active=False
  46        )
  47        cls.u3 = User.objects.create_user(username="staff", password="password")
  48        cls.u4 = User.objects.create(username="empty_password", password="")
  49        cls.u5 = User.objects.create(username="unmanageable_password", password="$")
  50        cls.u6 = User.objects.create(username="unknown_password", password="foo$bar")
  51
  52
  53class UserCreationFormTest(TestDataMixin, TestCase):
  54    def test_user_already_exists(self):
  55        data = {
  56            "username": "testclient",
  57            "password1": "test123",
  58            "password2": "test123",
  59        }
  60        form = UserCreationForm(data)
  61        self.assertFalse(form.is_valid())
  62        self.assertEqual(
  63            form["username"].errors,
  64            [str(User._meta.get_field("username").error_messages["unique"])],
  65        )
  66
  67    def test_invalid_data(self):
  68        data = {
  69            "username": "jsmith!",
  70            "password1": "test123",
  71            "password2": "test123",
  72        }
  73        form = UserCreationForm(data)
  74        self.assertFalse(form.is_valid())
  75        validator = next(
  76            v
  77            for v in User._meta.get_field("username").validators
  78            if v.code == "invalid"
  79        )
  80        self.assertEqual(form["username"].errors, [str(validator.message)])
  81
  82    def test_password_verification(self):
  83        # The verification password is incorrect.
  84        data = {
  85            "username": "jsmith",
  86            "password1": "test123",
  87            "password2": "test",
  88        }
  89        form = UserCreationForm(data)
  90        self.assertFalse(form.is_valid())
  91        self.assertEqual(
  92            form["password2"].errors, [str(form.error_messages["password_mismatch"])]
  93        )
  94
  95    def test_both_passwords(self):
  96        # One (or both) passwords weren't given
  97        data = {"username": "jsmith"}
  98        form = UserCreationForm(data)
  99        required_error = [str(Field.default_error_messages["required"])]
 100        self.assertFalse(form.is_valid())
 101        self.assertEqual(form["password1"].errors, required_error)
 102        self.assertEqual(form["password2"].errors, required_error)
 103
 104        data["password2"] = "test123"
 105        form = UserCreationForm(data)
 106        self.assertFalse(form.is_valid())
 107        self.assertEqual(form["password1"].errors, required_error)
 108        self.assertEqual(form["password2"].errors, [])
 109
 110    @mock.patch("django.contrib.auth.password_validation.password_changed")
 111    def test_success(self, password_changed):
 112        # The success case.
 113        data = {
 114            "username": "jsmith@example.com",
 115            "password1": "test123",
 116            "password2": "test123",
 117        }
 118        form = UserCreationForm(data)
 119        self.assertTrue(form.is_valid())
 120        form.save(commit=False)
 121        self.assertEqual(password_changed.call_count, 0)
 122        u = form.save()
 123        self.assertEqual(password_changed.call_count, 1)
 124        self.assertEqual(repr(u), "<User: jsmith@example.com>")
 125
 126    def test_unicode_username(self):
 127        data = {
 128            "username": "宝",
 129            "password1": "test123",
 130            "password2": "test123",
 131        }
 132        form = UserCreationForm(data)
 133        self.assertTrue(form.is_valid())
 134        u = form.save()
 135        self.assertEqual(u.username, "宝")
 136
 137    def test_normalize_username(self):
 138        # The normalization happens in AbstractBaseUser.clean() and ModelForm
 139        # validation calls Model.clean().
 140        ohm_username = "testΩ"  # U+2126 OHM SIGN
 141        data = {
 142            "username": ohm_username,
 143            "password1": "pwd2",
 144            "password2": "pwd2",
 145        }
 146        form = UserCreationForm(data)
 147        self.assertTrue(form.is_valid())
 148        user = form.save()
 149        self.assertNotEqual(user.username, ohm_username)
 150        self.assertEqual(user.username, "testΩ")  # U+03A9 GREEK CAPITAL LETTER OMEGA
 151
 152    def test_duplicate_normalized_unicode(self):
 153        """
 154        To prevent almost identical usernames, visually identical but differing
 155        by their unicode code points only, Unicode NFKC normalization should
 156        make appear them equal to Django.
 157        """
 158        omega_username = "iamtheΩ"  # U+03A9 GREEK CAPITAL LETTER OMEGA
 159        ohm_username = "iamtheΩ"  # U+2126 OHM SIGN
 160        self.assertNotEqual(omega_username, ohm_username)
 161        User.objects.create_user(username=omega_username, password="pwd")
 162        data = {
 163            "username": ohm_username,
 164            "password1": "pwd2",
 165            "password2": "pwd2",
 166        }
 167        form = UserCreationForm(data)
 168        self.assertFalse(form.is_valid())
 169        self.assertEqual(
 170            form.errors["username"], ["A user with that username already exists."]
 171        )
 172
 173    @override_settings(
 174        AUTH_PASSWORD_VALIDATORS=[
 175            {
 176                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 177            },
 178            {
 179                "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
 180                "OPTIONS": {"min_length": 12,},
 181            },
 182        ]
 183    )
 184    def test_validates_password(self):
 185        data = {
 186            "username": "testclient",
 187            "password1": "testclient",
 188            "password2": "testclient",
 189        }
 190        form = UserCreationForm(data)
 191        self.assertFalse(form.is_valid())
 192        self.assertEqual(len(form["password2"].errors), 2)
 193        self.assertIn(
 194            "The password is too similar to the username.", form["password2"].errors
 195        )
 196        self.assertIn(
 197            "This password is too short. It must contain at least 12 characters.",
 198            form["password2"].errors,
 199        )
 200
 201    def test_custom_form(self):
 202        class CustomUserCreationForm(UserCreationForm):
 203            class Meta(UserCreationForm.Meta):
 204                model = ExtensionUser
 205                fields = UserCreationForm.Meta.fields + ("date_of_birth",)
 206
 207        data = {
 208            "username": "testclient",
 209            "password1": "testclient",
 210            "password2": "testclient",
 211            "date_of_birth": "1988-02-24",
 212        }
 213        form = CustomUserCreationForm(data)
 214        self.assertTrue(form.is_valid())
 215
 216    def test_custom_form_with_different_username_field(self):
 217        class CustomUserCreationForm(UserCreationForm):
 218            class Meta(UserCreationForm.Meta):
 219                model = CustomUser
 220                fields = ("email", "date_of_birth")
 221
 222        data = {
 223            "email": "test@client222.com",
 224            "password1": "testclient",
 225            "password2": "testclient",
 226            "date_of_birth": "1988-02-24",
 227        }
 228        form = CustomUserCreationForm(data)
 229        self.assertTrue(form.is_valid())
 230
 231    def test_custom_form_hidden_username_field(self):
 232        class CustomUserCreationForm(UserCreationForm):
 233            class Meta(UserCreationForm.Meta):
 234                model = CustomUserWithoutIsActiveField
 235                fields = ("email",)  # without USERNAME_FIELD
 236
 237        data = {
 238            "email": "testclient@example.com",
 239            "password1": "testclient",
 240            "password2": "testclient",
 241        }
 242        form = CustomUserCreationForm(data)
 243        self.assertTrue(form.is_valid())
 244
 245    def test_password_whitespace_not_stripped(self):
 246        data = {
 247            "username": "testuser",
 248            "password1": "   testpassword   ",
 249            "password2": "   testpassword   ",
 250        }
 251        form = UserCreationForm(data)
 252        self.assertTrue(form.is_valid())
 253        self.assertEqual(form.cleaned_data["password1"], data["password1"])
 254        self.assertEqual(form.cleaned_data["password2"], data["password2"])
 255
 256    @override_settings(
 257        AUTH_PASSWORD_VALIDATORS=[
 258            {
 259                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 260            },
 261        ]
 262    )
 263    def test_password_help_text(self):
 264        form = UserCreationForm()
 265        self.assertEqual(
 266            form.fields["password1"].help_text,
 267            "<ul><li>Your password can’t be too similar to your other personal information.</li></ul>",
 268        )
 269
 270    @override_settings(
 271        AUTH_PASSWORD_VALIDATORS=[
 272            {
 273                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 274            },
 275        ]
 276    )
 277    def test_user_create_form_validates_password_with_all_data(self):
 278        """UserCreationForm password validation uses all of the form's data."""
 279
 280        class CustomUserCreationForm(UserCreationForm):
 281            class Meta(UserCreationForm.Meta):
 282                model = User
 283                fields = ("username", "email", "first_name", "last_name")
 284
 285        form = CustomUserCreationForm(
 286            {
 287                "username": "testuser",
 288                "password1": "testpassword",
 289                "password2": "testpassword",
 290                "first_name": "testpassword",
 291                "last_name": "lastname",
 292            }
 293        )
 294        self.assertFalse(form.is_valid())
 295        self.assertEqual(
 296            form.errors["password2"],
 297            ["The password is too similar to the first name."],
 298        )
 299
 300    def test_username_field_autocapitalize_none(self):
 301        form = UserCreationForm()
 302        self.assertEqual(
 303            form.fields["username"].widget.attrs.get("autocapitalize"), "none"
 304        )
 305
 306    def test_html_autocomplete_attributes(self):
 307        form = UserCreationForm()
 308        tests = (
 309            ("username", "username"),
 310            ("password1", "new-password"),
 311            ("password2", "new-password"),
 312        )
 313        for field_name, autocomplete in tests:
 314            with self.subTest(field_name=field_name, autocomplete=autocomplete):
 315                self.assertEqual(
 316                    form.fields[field_name].widget.attrs["autocomplete"], autocomplete
 317                )
 318
 319
 320# To verify that the login form rejects inactive users, use an authentication
 321# backend that allows them.
 322@override_settings(
 323    AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.AllowAllUsersModelBackend"]
 324)
 325class AuthenticationFormTest(TestDataMixin, TestCase):
 326    def test_invalid_username(self):
 327        # The user submits an invalid username.
 328
 329        data = {
 330            "username": "jsmith_does_not_exist",
 331            "password": "test123",
 332        }
 333        form = AuthenticationForm(None, data)
 334        self.assertFalse(form.is_valid())
 335        self.assertEqual(
 336            form.non_field_errors(),
 337            [
 338                form.error_messages["invalid_login"]
 339                % {"username": User._meta.get_field("username").verbose_name}
 340            ],
 341        )
 342
 343    def test_inactive_user(self):
 344        # The user is inactive.
 345        data = {
 346            "username": "inactive",
 347            "password": "password",
 348        }
 349        form = AuthenticationForm(None, data)
 350        self.assertFalse(form.is_valid())
 351        self.assertEqual(
 352            form.non_field_errors(), [str(form.error_messages["inactive"])]
 353        )
 354
 355    # Use an authentication backend that rejects inactive users.
 356    @override_settings(
 357        AUTHENTICATION_BACKENDS=["django.contrib.auth.backends.ModelBackend"]
 358    )
 359    def test_inactive_user_incorrect_password(self):
 360        """An invalid login doesn't leak the inactive status of a user."""
 361        data = {
 362            "username": "inactive",
 363            "password": "incorrect",
 364        }
 365        form = AuthenticationForm(None, data)
 366        self.assertFalse(form.is_valid())
 367        self.assertEqual(
 368            form.non_field_errors(),
 369            [
 370                form.error_messages["invalid_login"]
 371                % {"username": User._meta.get_field("username").verbose_name}
 372            ],
 373        )
 374
 375    def test_login_failed(self):
 376        signal_calls = []
 377
 378        def signal_handler(**kwargs):
 379            signal_calls.append(kwargs)
 380
 381        user_login_failed.connect(signal_handler)
 382        fake_request = object()
 383        try:
 384            form = AuthenticationForm(
 385                fake_request, {"username": "testclient", "password": "incorrect",}
 386            )
 387            self.assertFalse(form.is_valid())
 388            self.assertIs(signal_calls[0]["request"], fake_request)
 389        finally:
 390            user_login_failed.disconnect(signal_handler)
 391
 392    def test_inactive_user_i18n(self):
 393        with self.settings(USE_I18N=True), translation.override(
 394            "pt-br", deactivate=True
 395        ):
 396            # The user is inactive.
 397            data = {
 398                "username": "inactive",
 399                "password": "password",
 400            }
 401            form = AuthenticationForm(None, data)
 402            self.assertFalse(form.is_valid())
 403            self.assertEqual(
 404                form.non_field_errors(), [str(form.error_messages["inactive"])]
 405            )
 406
 407    # Use an authentication backend that allows inactive users.
 408    @override_settings(
 409        AUTHENTICATION_BACKENDS=[
 410            "django.contrib.auth.backends.AllowAllUsersModelBackend"
 411        ]
 412    )
 413    def test_custom_login_allowed_policy(self):
 414        # The user is inactive, but our custom form policy allows them to log in.
 415        data = {
 416            "username": "inactive",
 417            "password": "password",
 418        }
 419
 420        class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm):
 421            def confirm_login_allowed(self, user):
 422                pass
 423
 424        form = AuthenticationFormWithInactiveUsersOkay(None, data)
 425        self.assertTrue(form.is_valid())
 426
 427        # If we want to disallow some logins according to custom logic,
 428        # we should raise a django.forms.ValidationError in the form.
 429        class PickyAuthenticationForm(AuthenticationForm):
 430            def confirm_login_allowed(self, user):
 431                if user.username == "inactive":
 432                    raise forms.ValidationError("This user is disallowed.")
 433                raise forms.ValidationError("Sorry, nobody's allowed in.")
 434
 435        form = PickyAuthenticationForm(None, data)
 436        self.assertFalse(form.is_valid())
 437        self.assertEqual(form.non_field_errors(), ["This user is disallowed."])
 438
 439        data = {
 440            "username": "testclient",
 441            "password": "password",
 442        }
 443        form = PickyAuthenticationForm(None, data)
 444        self.assertFalse(form.is_valid())
 445        self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."])
 446
 447    def test_success(self):
 448        # The success case
 449        data = {
 450            "username": "testclient",
 451            "password": "password",
 452        }
 453        form = AuthenticationForm(None, data)
 454        self.assertTrue(form.is_valid())
 455        self.assertEqual(form.non_field_errors(), [])
 456
 457    def test_unicode_username(self):
 458        User.objects.create_user(username="Σαρα", password="pwd")
 459        data = {
 460            "username": "Σαρα",
 461            "password": "pwd",
 462        }
 463        form = AuthenticationForm(None, data)
 464        self.assertTrue(form.is_valid())
 465        self.assertEqual(form.non_field_errors(), [])
 466
 467    @override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
 468    def test_username_field_max_length_matches_user_model(self):
 469        self.assertEqual(CustomEmailField._meta.get_field("username").max_length, 255)
 470        data = {
 471            "username": "u" * 255,
 472            "password": "pwd",
 473            "email": "test@example.com",
 474        }
 475        CustomEmailField.objects.create_user(**data)
 476        form = AuthenticationForm(None, data)
 477        self.assertEqual(form.fields["username"].max_length, 255)
 478        self.assertEqual(form.fields["username"].widget.attrs.get("maxlength"), 255)
 479        self.assertEqual(form.errors, {})
 480
 481    @override_settings(AUTH_USER_MODEL="auth_tests.IntegerUsernameUser")
 482    def test_username_field_max_length_defaults_to_254(self):
 483        self.assertIsNone(IntegerUsernameUser._meta.get_field("username").max_length)
 484        data = {
 485            "username": "0123456",
 486            "password": "password",
 487        }
 488        IntegerUsernameUser.objects.create_user(**data)
 489        form = AuthenticationForm(None, data)
 490        self.assertEqual(form.fields["username"].max_length, 254)
 491        self.assertEqual(form.fields["username"].widget.attrs.get("maxlength"), 254)
 492        self.assertEqual(form.errors, {})
 493
 494    def test_username_field_label(self):
 495        class CustomAuthenticationForm(AuthenticationForm):
 496            username = CharField(label="Name", max_length=75)
 497
 498        form = CustomAuthenticationForm()
 499        self.assertEqual(form["username"].label, "Name")
 500
 501    def test_username_field_label_not_set(self):
 502        class CustomAuthenticationForm(AuthenticationForm):
 503            username = CharField()
 504
 505        form = CustomAuthenticationForm()
 506        username_field = User._meta.get_field(User.USERNAME_FIELD)
 507        self.assertEqual(
 508            form.fields["username"].label, capfirst(username_field.verbose_name)
 509        )
 510
 511    def test_username_field_autocapitalize_none(self):
 512        form = AuthenticationForm()
 513        self.assertEqual(
 514            form.fields["username"].widget.attrs.get("autocapitalize"), "none"
 515        )
 516
 517    def test_username_field_label_empty_string(self):
 518        class CustomAuthenticationForm(AuthenticationForm):
 519            username = CharField(label="")
 520
 521        form = CustomAuthenticationForm()
 522        self.assertEqual(form.fields["username"].label, "")
 523
 524    def test_password_whitespace_not_stripped(self):
 525        data = {
 526            "username": "testuser",
 527            "password": " pass ",
 528        }
 529        form = AuthenticationForm(None, data)
 530        form.is_valid()  # Not necessary to have valid credentails for the test.
 531        self.assertEqual(form.cleaned_data["password"], data["password"])
 532
 533    @override_settings(AUTH_USER_MODEL="auth_tests.IntegerUsernameUser")
 534    def test_integer_username(self):
 535        class CustomAuthenticationForm(AuthenticationForm):
 536            username = IntegerField()
 537
 538        user = IntegerUsernameUser.objects.create_user(username=0, password="pwd")
 539        data = {
 540            "username": 0,
 541            "password": "pwd",
 542        }
 543        form = CustomAuthenticationForm(None, data)
 544        self.assertTrue(form.is_valid())
 545        self.assertEqual(form.cleaned_data["username"], data["username"])
 546        self.assertEqual(form.cleaned_data["password"], data["password"])
 547        self.assertEqual(form.errors, {})
 548        self.assertEqual(form.user_cache, user)
 549
 550    def test_get_invalid_login_error(self):
 551        error = AuthenticationForm().get_invalid_login_error()
 552        self.assertIsInstance(error, forms.ValidationError)
 553        self.assertEqual(
 554            error.message,
 555            "Please enter a correct %(username)s and password. Note that both "
 556            "fields may be case-sensitive.",
 557        )
 558        self.assertEqual(error.code, "invalid_login")
 559        self.assertEqual(error.params, {"username": "username"})
 560
 561    def test_html_autocomplete_attributes(self):
 562        form = AuthenticationForm()
 563        tests = (
 564            ("username", "username"),
 565            ("password", "current-password"),
 566        )
 567        for field_name, autocomplete in tests:
 568            with self.subTest(field_name=field_name, autocomplete=autocomplete):
 569                self.assertEqual(
 570                    form.fields[field_name].widget.attrs["autocomplete"], autocomplete
 571                )
 572
 573
 574class SetPasswordFormTest(TestDataMixin, TestCase):
 575    def test_password_verification(self):
 576        # The two new passwords do not match.
 577        user = User.objects.get(username="testclient")
 578        data = {
 579            "new_password1": "abc123",
 580            "new_password2": "abc",
 581        }
 582        form = SetPasswordForm(user, data)
 583        self.assertFalse(form.is_valid())
 584        self.assertEqual(
 585            form["new_password2"].errors,
 586            [str(form.error_messages["password_mismatch"])],
 587        )
 588
 589    @mock.patch("django.contrib.auth.password_validation.password_changed")
 590    def test_success(self, password_changed):
 591        user = User.objects.get(username="testclient")
 592        data = {
 593            "new_password1": "abc123",
 594            "new_password2": "abc123",
 595        }
 596        form = SetPasswordForm(user, data)
 597        self.assertTrue(form.is_valid())
 598        form.save(commit=False)
 599        self.assertEqual(password_changed.call_count, 0)
 600        form.save()
 601        self.assertEqual(password_changed.call_count, 1)
 602
 603    @override_settings(
 604        AUTH_PASSWORD_VALIDATORS=[
 605            {
 606                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 607            },
 608            {
 609                "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
 610                "OPTIONS": {"min_length": 12,},
 611            },
 612        ]
 613    )
 614    def test_validates_password(self):
 615        user = User.objects.get(username="testclient")
 616        data = {
 617            "new_password1": "testclient",
 618            "new_password2": "testclient",
 619        }
 620        form = SetPasswordForm(user, data)
 621        self.assertFalse(form.is_valid())
 622        self.assertEqual(len(form["new_password2"].errors), 2)
 623        self.assertIn(
 624            "The password is too similar to the username.", form["new_password2"].errors
 625        )
 626        self.assertIn(
 627            "This password is too short. It must contain at least 12 characters.",
 628            form["new_password2"].errors,
 629        )
 630
 631    def test_password_whitespace_not_stripped(self):
 632        user = User.objects.get(username="testclient")
 633        data = {
 634            "new_password1": "   password   ",
 635            "new_password2": "   password   ",
 636        }
 637        form = SetPasswordForm(user, data)
 638        self.assertTrue(form.is_valid())
 639        self.assertEqual(form.cleaned_data["new_password1"], data["new_password1"])
 640        self.assertEqual(form.cleaned_data["new_password2"], data["new_password2"])
 641
 642    @override_settings(
 643        AUTH_PASSWORD_VALIDATORS=[
 644            {
 645                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 646            },
 647            {
 648                "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
 649                "OPTIONS": {"min_length": 12,},
 650            },
 651        ]
 652    )
 653    def test_help_text_translation(self):
 654        french_help_texts = [
 655            "Votre mot de passe ne peut pas trop ressembler à vos autres informations personnelles.",
 656            "Votre mot de passe doit contenir au minimum 12 caractères.",
 657        ]
 658        form = SetPasswordForm(self.u1)
 659        with translation.override("fr"):
 660            html = form.as_p()
 661            for french_text in french_help_texts:
 662                self.assertIn(french_text, html)
 663
 664    def test_html_autocomplete_attributes(self):
 665        form = SetPasswordForm(self.u1)
 666        tests = (
 667            ("new_password1", "new-password"),
 668            ("new_password2", "new-password"),
 669        )
 670        for field_name, autocomplete in tests:
 671            with self.subTest(field_name=field_name, autocomplete=autocomplete):
 672                self.assertEqual(
 673                    form.fields[field_name].widget.attrs["autocomplete"], autocomplete
 674                )
 675
 676
 677class PasswordChangeFormTest(TestDataMixin, TestCase):
 678    def test_incorrect_password(self):
 679        user = User.objects.get(username="testclient")
 680        data = {
 681            "old_password": "test",
 682            "new_password1": "abc123",
 683            "new_password2": "abc123",
 684        }
 685        form = PasswordChangeForm(user, data)
 686        self.assertFalse(form.is_valid())
 687        self.assertEqual(
 688            form["old_password"].errors,
 689            [str(form.error_messages["password_incorrect"])],
 690        )
 691
 692    def test_password_verification(self):
 693        # The two new passwords do not match.
 694        user = User.objects.get(username="testclient")
 695        data = {
 696            "old_password": "password",
 697            "new_password1": "abc123",
 698            "new_password2": "abc",
 699        }
 700        form = PasswordChangeForm(user, data)
 701        self.assertFalse(form.is_valid())
 702        self.assertEqual(
 703            form["new_password2"].errors,
 704            [str(form.error_messages["password_mismatch"])],
 705        )
 706
 707    @mock.patch("django.contrib.auth.password_validation.password_changed")
 708    def test_success(self, password_changed):
 709        # The success case.
 710        user = User.objects.get(username="testclient")
 711        data = {
 712            "old_password": "password",
 713            "new_password1": "abc123",
 714            "new_password2": "abc123",
 715        }
 716        form = PasswordChangeForm(user, data)
 717        self.assertTrue(form.is_valid())
 718        form.save(commit=False)
 719        self.assertEqual(password_changed.call_count, 0)
 720        form.save()
 721        self.assertEqual(password_changed.call_count, 1)
 722
 723    def test_field_order(self):
 724        # Regression test - check the order of fields:
 725        user = User.objects.get(username="testclient")
 726        self.assertEqual(
 727            list(PasswordChangeForm(user, {}).fields),
 728            ["old_password", "new_password1", "new_password2"],
 729        )
 730
 731    def test_password_whitespace_not_stripped(self):
 732        user = User.objects.get(username="testclient")
 733        user.set_password("   oldpassword   ")
 734        data = {
 735            "old_password": "   oldpassword   ",
 736            "new_password1": " pass ",
 737            "new_password2": " pass ",
 738        }
 739        form = PasswordChangeForm(user, data)
 740        self.assertTrue(form.is_valid())
 741        self.assertEqual(form.cleaned_data["old_password"], data["old_password"])
 742        self.assertEqual(form.cleaned_data["new_password1"], data["new_password1"])
 743        self.assertEqual(form.cleaned_data["new_password2"], data["new_password2"])
 744
 745    def test_html_autocomplete_attributes(self):
 746        user = User.objects.get(username="testclient")
 747        form = PasswordChangeForm(user)
 748        self.assertEqual(
 749            form.fields["old_password"].widget.attrs["autocomplete"], "current-password"
 750        )
 751
 752
 753class UserChangeFormTest(TestDataMixin, TestCase):
 754    def test_username_validity(self):
 755        user = User.objects.get(username="testclient")
 756        data = {"username": "not valid"}
 757        form = UserChangeForm(data, instance=user)
 758        self.assertFalse(form.is_valid())
 759        validator = next(
 760            v
 761            for v in User._meta.get_field("username").validators
 762            if v.code == "invalid"
 763        )
 764        self.assertEqual(form["username"].errors, [str(validator.message)])
 765
 766    def test_bug_14242(self):
 767        # A regression test, introduce by adding an optimization for the
 768        # UserChangeForm.
 769
 770        class MyUserForm(UserChangeForm):
 771            def __init__(self, *args, **kwargs):
 772                super().__init__(*args, **kwargs)
 773                self.fields[
 774                    "groups"
 775                ].help_text = "These groups give users different permissions"
 776
 777            class Meta(UserChangeForm.Meta):
 778                fields = ("groups",)
 779
 780        # Just check we can create it
 781        MyUserForm({})
 782
 783    def test_unusable_password(self):
 784        user = User.objects.get(username="empty_password")
 785        user.set_unusable_password()
 786        user.save()
 787        form = UserChangeForm(instance=user)
 788        self.assertIn(_("No password set."), form.as_table())
 789
 790    def test_bug_17944_empty_password(self):
 791        user = User.objects.get(username="empty_password")
 792        form = UserChangeForm(instance=user)
 793        self.assertIn(_("No password set."), form.as_table())
 794
 795    def test_bug_17944_unmanageable_password(self):
 796        user = User.objects.get(username="unmanageable_password")
 797        form = UserChangeForm(instance=user)
 798        self.assertIn(
 799            _("Invalid password format or unknown hashing algorithm."), form.as_table()
 800        )
 801
 802    def test_bug_17944_unknown_password_algorithm(self):
 803        user = User.objects.get(username="unknown_password")
 804        form = UserChangeForm(instance=user)
 805        self.assertIn(
 806            _("Invalid password format or unknown hashing algorithm."), form.as_table()
 807        )
 808
 809    def test_bug_19133(self):
 810        "The change form does not return the password value"
 811        # Use the form to construct the POST data
 812        user = User.objects.get(username="testclient")
 813        form_for_data = UserChangeForm(instance=user)
 814        post_data = form_for_data.initial
 815
 816        # The password field should be readonly, so anything
 817        # posted here should be ignored; the form will be
 818        # valid, and give back the 'initial' value for the
 819        # password field.
 820        post_data["password"] = "new password"
 821        form = UserChangeForm(instance=user, data=post_data)
 822
 823        self.assertTrue(form.is_valid())
 824        # original hashed password contains $
 825        self.assertIn("$", form.cleaned_data["password"])
 826
 827    def test_bug_19349_bound_password_field(self):
 828        user = User.objects.get(username="testclient")
 829        form = UserChangeForm(data={}, instance=user)
 830        # When rendering the bound password field,
 831        # ReadOnlyPasswordHashWidget needs the initial
 832        # value to render correctly
 833        self.assertEqual(form.initial["password"], form["password"].value())
 834
 835    def test_custom_form(self):
 836        class CustomUserChangeForm(UserChangeForm):
 837            class Meta(UserChangeForm.Meta):
 838                model = ExtensionUser
 839                fields = (
 840                    "username",
 841                    "password",
 842                    "date_of_birth",
 843                )
 844
 845        user = User.objects.get(username="testclient")
 846        data = {
 847            "username": "testclient",
 848            "password": "testclient",
 849            "date_of_birth": "1998-02-24",
 850        }
 851        form = CustomUserChangeForm(data, instance=user)
 852        self.assertTrue(form.is_valid())
 853        form.save()
 854        self.assertEqual(form.cleaned_data["username"], "testclient")
 855        self.assertEqual(form.cleaned_data["date_of_birth"], datetime.date(1998, 2, 24))
 856
 857    def test_password_excluded(self):
 858        class UserChangeFormWithoutPassword(UserChangeForm):
 859            password = None
 860
 861            class Meta:
 862                model = User
 863                exclude = ["password"]
 864
 865        form = UserChangeFormWithoutPassword()
 866        self.assertNotIn("password", form.fields)
 867
 868    def test_username_field_autocapitalize_none(self):
 869        form = UserChangeForm()
 870        self.assertEqual(
 871            form.fields["username"].widget.attrs.get("autocapitalize"), "none"
 872        )
 873
 874
 875@override_settings(TEMPLATES=AUTH_TEMPLATES)
 876class PasswordResetFormTest(TestDataMixin, TestCase):
 877    @classmethod
 878    def setUpClass(cls):
 879        super().setUpClass()
 880        # This cleanup is necessary because contrib.sites cache
 881        # makes tests interfere with each other, see #11505
 882        Site.objects.clear_cache()
 883
 884    def create_dummy_user(self):
 885        """
 886        Create a user and return a tuple (user_object, username, email).
 887        """
 888        username = "jsmith"
 889        email = "jsmith@example.com"
 890        user = User.objects.create_user(username, email, "test123")
 891        return (user, username, email)
 892
 893    def test_invalid_email(self):
 894        data = {"email": "not valid"}
 895        form = PasswordResetForm(data)
 896        self.assertFalse(form.is_valid())
 897        self.assertEqual(form["email"].errors, [_("Enter a valid email address.")])
 898
 899    def test_nonexistent_email(self):
 900        """
 901        Test nonexistent email address. This should not fail because it would
 902        expose information about registered users.
 903        """
 904        data = {"email": "foo@bar.com"}
 905        form = PasswordResetForm(data)
 906        self.assertTrue(form.is_valid())
 907        self.assertEqual(len(mail.outbox), 0)
 908
 909    def test_cleaned_data(self):
 910        (user, username, email) = self.create_dummy_user()
 911        data = {"email": email}
 912        form = PasswordResetForm(data)
 913        self.assertTrue(form.is_valid())
 914        form.save(domain_override="example.com")
 915        self.assertEqual(form.cleaned_data["email"], email)
 916        self.assertEqual(len(mail.outbox), 1)
 917
 918    def test_custom_email_subject(self):
 919        data = {"email": "testclient@example.com"}
 920        form = PasswordResetForm(data)
 921        self.assertTrue(form.is_valid())
 922        # Since we're not providing a request object, we must provide a
 923        # domain_override to prevent the save operation from failing in the
 924        # potential case where contrib.sites is not installed. Refs #16412.
 925        form.save(domain_override="example.com")
 926        self.assertEqual(len(mail.outbox), 1)
 927        self.assertEqual(mail.outbox[0].subject, "Custom password reset on example.com")
 928
 929    def test_custom_email_constructor(self):
 930        data = {"email": "testclient@example.com"}
 931
 932        class CustomEmailPasswordResetForm(PasswordResetForm):
 933            def send_mail(
 934                self,
 935                subject_template_name,
 936                email_template_name,
 937                context,
 938                from_email,
 939                to_email,
 940                html_email_template_name=None,
 941            ):
 942                EmailMultiAlternatives(
 943                    "Forgot your password?",
 944                    "Sorry to hear you forgot your password.",
 945                    None,
 946                    [to_email],
 947                    ["site_monitor@example.com"],
 948                    headers={"Reply-To": "webmaster@example.com"},
 949                    alternatives=[
 950                        ("Really sorry to hear you forgot your password.", "text/html")
 951                    ],
 952                ).send()
 953
 954        form = CustomEmailPasswordResetForm(data)
 955        self.assertTrue(form.is_valid())
 956        # Since we're not providing a request object, we must provide a
 957        # domain_override to prevent the save operation from failing in the
 958        # potential case where contrib.sites is not installed. Refs #16412.
 959        form.save(domain_override="example.com")
 960        self.assertEqual(len(mail.outbox), 1)
 961        self.assertEqual(mail.outbox[0].subject, "Forgot your password?")
 962        self.assertEqual(mail.outbox[0].bcc, ["site_monitor@example.com"])
 963        self.assertEqual(mail.outbox[0].content_subtype, "plain")
 964
 965    def test_preserve_username_case(self):
 966        """
 967        Preserve the case of the user name (before the @ in the email address)
 968        when creating a user (#5605).
 969        """
 970        user = User.objects.create_user("forms_test2", "tesT@EXAMple.com", "test")
 971        self.assertEqual(user.email, "tesT@example.com")
 972        user = User.objects.create_user("forms_test3", "tesT", "test")
 973        self.assertEqual(user.email, "tesT")
 974
 975    def test_inactive_user(self):
 976        """
 977        Inactive user cannot receive password reset email.
 978        """
 979        (user, username, email) = self.create_dummy_user()
 980        user.is_active = False
 981        user.save()
 982        form = PasswordResetForm({"email": email})
 983        self.assertTrue(form.is_valid())
 984        form.save()
 985        self.assertEqual(len(mail.outbox), 0)
 986
 987    def test_unusable_password(self):
 988        user = User.objects.create_user("testuser", "test@example.com", "test")
 989        data = {"email": "test@example.com"}
 990        form = PasswordResetForm(data)
 991        self.assertTrue(form.is_valid())
 992        user.set_unusable_password()
 993        user.save()
 994        form = PasswordResetForm(data)
 995        # The form itself is valid, but no email is sent
 996        self.assertTrue(form.is_valid())
 997        form.save()
 998        self.assertEqual(len(mail.outbox), 0)
 999
1000    def test_save_plaintext_email(self):
1001        """
1002        Test the PasswordResetForm.save() method with no html_email_template_name
1003        parameter passed in.
1004        Test to ensure original behavior is unchanged after the parameter was added.
1005        """
1006        (user, username, email) = self.create_dummy_user()
1007        form = PasswordResetForm({"email": email})
1008        self.assertTrue(form.is_valid())
1009        form.save()
1010        self.assertEqual(len(mail.outbox), 1)
1011        message = mail.outbox[0].message()
1012        self.assertFalse(message.is_multipart())
1013        self.assertEqual(message.get_content_type(), "text/plain")
1014        self.assertEqual(message.get("subject"), "Custom password reset on example.com")
1015        self.assertEqual(len(mail.outbox[0].alternatives), 0)
1016        self.assertEqual(message.get_all("to"), [email])
1017        self.assertTrue(
1018            re.match(r"^http://example.com/reset/[\w+/-]", message.get_payload())
1019        )
1020
1021    def test_save_html_email_template_name(self):
1022        """
1023        Test the PasswordResetForm.save() method with html_email_template_name
1024        parameter specified.
1025        Test to ensure that a multipart email is sent with both text/plain
1026        and text/html parts.
1027        """
1028        (user, username, email) = self.create_dummy_user()
1029        form = PasswordResetForm({"email": email})
1030        self.assertTrue(form.is_valid())
1031        form.save(
1032            html_email_template_name="registration/html_password_reset_email.html"
1033        )
1034        self.assertEqual(len(mail.outbox), 1)
1035        self.assertEqual(len(mail.outbox[0].alternatives), 1)
1036        message = mail.outbox[0].message()
1037        self.assertEqual(message.get("subject"), "Custom password reset on example.com")
1038        self.assertEqual(len(message.get_payload()), 2)
1039        self.assertTrue(message.is_multipart())
1040        self.assertEqual(message.get_payload(0).get_content_type(), "text/plain")
1041        self.assertEqual(message.get_payload(1).get_content_type(), "text/html")
1042        self.assertEqual(message.get_all("to"), [email])
1043        self.assertTrue(
1044            re.match(
1045                r"^http://example.com/reset/[\w/-]+",
1046                message.get_payload(0).get_payload(),
1047            )
1048        )
1049        self.assertTrue(
1050            re.match(
1051                r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$',
1052                message.get_payload(1).get_payload(),
1053            )
1054        )
1055
1056    @override_settings(AUTH_USER_MODEL="auth_tests.CustomEmailField")
1057    def test_custom_email_field(self):
1058        email = "test@mail.com"
1059        CustomEmailField.objects.create_user("test name", "test password", email)
1060        form = PasswordResetForm({"email": email})
1061        self.assertTrue(form.is_valid())
1062        form.save()
1063        self.assertEqual(form.cleaned_data["email"], email)
1064        self.assertEqual(len(mail.outbox), 1)
1065        self.assertEqual(mail.outbox[0].to, [email])
1066
1067    def test_html_autocomplete_attributes(self):
1068        form = PasswordResetForm()
1069        self.assertEqual(form.fields["email"].widget.attrs["autocomplete"], "email")
1070
1071
1072class ReadOnlyPasswordHashTest(SimpleTestCase):
1073    def test_bug_19349_render_with_none_value(self):
1074        # Rendering the widget with value set to None
1075        # mustn't raise an exception.
1076        widget = ReadOnlyPasswordHashWidget()
1077        html = widget.render(name="password", value=None, attrs={})
1078        self.assertIn(_("No password set."), html)
1079
1080    @override_settings(
1081        PASSWORD_HASHERS=["django.contrib.auth.hashers.PBKDF2PasswordHasher"]
1082    )
1083    def test_render(self):
1084        widget = ReadOnlyPasswordHashWidget()
1085        value = "pbkdf2_sha256$100000$a6Pucb1qSFcD$WmCkn9Hqidj48NVe5x0FEM6A9YiOqQcl/83m2Z5udm0="
1086        self.assertHTMLEqual(
1087            widget.render("name", value, {"id": "id_password"}),
1088            """
1089            <div id="id_password">
1090                <strong>algorithm</strong>: pbkdf2_sha256
1091                <strong>iterations</strong>: 100000
1092                <strong>salt</strong>: a6Pucb******
1093                <strong>hash</strong>: WmCkn9**************************************
1094            </div>
1095            """,
1096        )
1097
1098    def test_readonly_field_has_changed(self):
1099        field = ReadOnlyPasswordHashField()
1100        self.assertFalse(field.has_changed("aaa", "bbb"))
1101
1102
1103class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
1104    @mock.patch("django.contrib.auth.password_validation.password_changed")
1105    def test_success(self, password_changed):
1106        user = User.objects.get(username="testclient")
1107        data = {
1108            "password1": "test123",
1109            "password2": "test123",
1110        }
1111        form = AdminPasswordChangeForm(user, data)
1112        self.assertTrue(form.is_valid())
1113        form.save(commit=False)
1114        self.assertEqual(password_changed.call_count, 0)
1115        form.save()
1116        self.assertEqual(password_changed.call_count, 1)
1117
1118    def test_password_whitespace_not_stripped(self):
1119        user = User.objects.get(username="testclient")
1120        data = {
1121            "password1": " pass ",
1122            "password2": " pass ",
1123        }
1124        form = AdminPasswordChangeForm(user, data)
1125        self.assertTrue(form.is_valid())
1126        self.assertEqual(form.cleaned_data["password1"], data["password1"])
1127        self.assertEqual(form.cleaned_data["password2"], data["password2"])
1128
1129    def test_non_matching_passwords(self):
1130        user = User.objects.get(username="testclient")
1131        data = {"password1": "password1", "password2": "password2"}
1132        form = AdminPasswordChangeForm(user, data)
1133        self.assertEqual(
1134            form.errors["password2"], [form.error_messages["password_mismatch"]]
1135        )
1136
1137    def test_missing_passwords(self):
1138        user = User.objects.get(username="testclient")
1139        data = {"password1": "", "password2": ""}
1140        form = AdminPasswordChangeForm(user, data)
1141        required_error = [Field.default_error_messages["required"]]
1142        self.assertEqual(form.errors["password1"], required_error)
1143        self.assertEqual(form.errors["password2"], required_error)
1144
1145    def test_one_password(self):
1146        user = User.objects.get(username="testclient")
1147        form1 = AdminPasswordChangeForm(user, {"password1": "", "password2": "test"})
1148        required_error = [Field.default_error_messages["required"]]
1149        self.assertEqual(form1.errors["password1"], required_error)
1150        self.assertNotIn("password2", form1.errors)
1151        form2 = AdminPasswordChangeForm(user, {"password1": "test", "password2": ""})
1152        self.assertEqual(form2.errors["password2"], required_error)
1153        self.assertNotIn("password1", form2.errors)
1154
1155    def test_html_autocomplete_attributes(self):
1156        user = User.objects.get(username="testclient")
1157        form = AdminPasswordChangeForm(user)
1158        tests = (
1159            ("password1", "new-password"),
1160            ("password2", "new-password"),
1161        )
1162        for field_name, autocomplete in tests:
1163            with self.subTest(field_name=field_name, autocomplete=autocomplete):
1164                self.assertEqual(
1165                    form.fields[field_name].widget.attrs["autocomplete"], autocomplete
1166                )

tests/admin_widgets/test_autocomplete_widget.py

  1from django import forms
  2from django.contrib import admin
  3from django.contrib.admin.widgets import AutocompleteSelect
  4from django.forms import ModelChoiceField
  5from django.test import TestCase, override_settings
  6from django.utils import translation
  7
  8from .models import Album, Band
  9
 10
 11class AlbumForm(forms.ModelForm):
 12    class Meta:
 13        model = Album
 14        fields = ["band", "featuring"]
 15        widgets = {
 16            "band": AutocompleteSelect(
 17                Album._meta.get_field("band").remote_field,
 18                admin.site,
 19                attrs={"class": "my-class"},
 20            ),
 21            "featuring": AutocompleteSelect(
 22                Album._meta.get_field("featuring").remote_field, admin.site,
 23            ),
 24        }
 25
 26
 27class NotRequiredBandForm(forms.Form):
 28    band = ModelChoiceField(
 29        queryset=Album.objects.all(),
 30        widget=AutocompleteSelect(
 31            Album._meta.get_field("band").remote_field, admin.site
 32        ),
 33        required=False,
 34    )
 35
 36
 37class RequiredBandForm(forms.Form):
 38    band = ModelChoiceField(
 39        queryset=Album.objects.all(),
 40        widget=AutocompleteSelect(
 41            Album._meta.get_field("band").remote_field, admin.site
 42        ),
 43        required=True,
 44    )
 45
 46
 47@override_settings(ROOT_URLCONF="admin_widgets.urls")
 48class AutocompleteMixinTests(TestCase):
 49    empty_option = '<option value=""></option>'
 50    maxDiff = 1000
 51
 52    def test_build_attrs(self):
 53        form = AlbumForm()
 54        attrs = form["band"].field.widget.get_context(
 55            name="my_field", value=None, attrs={}
 56        )["widget"]["attrs"]
 57        self.assertEqual(
 58            attrs,
 59            {
 60                "class": "my-class admin-autocomplete",
 61                "data-ajax--cache": "true",
 62                "data-ajax--delay": 250,
 63                "data-ajax--type": "GET",
 64                "data-ajax--url": "/admin_widgets/band/autocomplete/",
 65                "data-theme": "admin-autocomplete",
 66                "data-allow-clear": "false",
 67                "data-placeholder": "",
 68            },
 69        )
 70
 71    def test_build_attrs_no_custom_class(self):
 72        form = AlbumForm()
 73        attrs = form["featuring"].field.widget.get_context(
 74            name="name", value=None, attrs={}
 75        )["widget"]["attrs"]
 76        self.assertEqual(attrs["class"], "admin-autocomplete")
 77
 78    def test_build_attrs_not_required_field(self):
 79        form = NotRequiredBandForm()
 80        attrs = form["band"].field.widget.build_attrs({})
 81        self.assertJSONEqual(attrs["data-allow-clear"], True)
 82
 83    def test_build_attrs_required_field(self):
 84        form = RequiredBandForm()
 85        attrs = form["band"].field.widget.build_attrs({})
 86        self.assertJSONEqual(attrs["data-allow-clear"], False)
 87
 88    def test_get_url(self):
 89        rel = Album._meta.get_field("band").remote_field
 90        w = AutocompleteSelect(rel, admin.site)
 91        url = w.get_url()
 92        self.assertEqual(url, "/admin_widgets/band/autocomplete/")
 93
 94    def test_render_options(self):
 95        beatles = Band.objects.create(name="The Beatles", style="rock")
 96        who = Band.objects.create(name="The Who", style="rock")
 97        # With 'band', a ForeignKey.
 98        form = AlbumForm(initial={"band": beatles.pk})
 99        output = form.as_table()
100        selected_option = (
101            '<option value="%s" selected>The Beatles</option>' % beatles.pk
102        )
103        option = '<option value="%s">The Who</option>' % who.pk
104        self.assertIn(selected_option, output)
105        self.assertNotIn(option, output)
106        # With 'featuring', a ManyToManyField.
107        form = AlbumForm(initial={"featuring": [beatles.pk, who.pk]})
108        output = form.as_table()
109        selected_option = (
110            '<option value="%s" selected>The Beatles</option>' % beatles.pk
111        )
112        option = '<option value="%s" selected>The Who</option>' % who.pk
113        self.assertIn(selected_option, output)
114        self.assertIn(option, output)
115
116    def test_render_options_required_field(self):
117        """Empty option is present if the field isn't required."""
118        form = NotRequiredBandForm()
119        output = form.as_table()
120        self.assertIn(self.empty_option, output)
121
122    def test_render_options_not_required_field(self):
123        """Empty option isn't present if the field isn't required."""
124        form = RequiredBandForm()
125        output = form.as_table()
126        self.assertNotIn(self.empty_option, output)
127
128    def test_media(self):
129        rel = Album._meta.get_field("band").remote_field
130        base_files = (
131            "admin/js/vendor/jquery/jquery.min.js",
132            "admin/js/vendor/select2/select2.full.min.js",
133            # Language file is inserted here.
134            "admin/js/jquery.init.js",
135            "admin/js/autocomplete.js",
136        )
137        languages = (
138            ("de", "de"),
139            # Language with code 00 does not exist.
140            ("00", None),
141            # Language files are case sensitive.
142            ("sr-cyrl", "sr-Cyrl"),
143            ("zh-hans", "zh-CN"),
144            ("zh-hant", "zh-TW"),
145        )
146        for lang, select_lang in languages:
147            with self.subTest(lang=lang):
148                if select_lang:
149                    expected_files = (
150                        base_files[:2]
151                        + (("admin/js/vendor/select2/i18n/%s.js" % select_lang),)
152                        + base_files[2:]
153                    )
154                else:
155                    expected_files = base_files
156                with translation.override(lang):
157                    self.assertEqual(
158                        AutocompleteSelect(rel, admin.site).media._js,
159                        list(expected_files),
160                    )

tests/modeladmin/test_checks.py

   1from django import forms
   2from django.contrib.admin import BooleanFieldListFilter, SimpleListFilter
   3from django.contrib.admin.options import VERTICAL, ModelAdmin, TabularInline
   4from django.contrib.admin.sites import AdminSite
   5from django.core.checks import Error
   6from django.db.models import F, Field, Model
   7from django.db.models.functions import Upper
   8from django.forms.models import BaseModelFormSet
   9from django.test import SimpleTestCase
  10
  11from .models import (
  12    Band,
  13    Song,
  14    User,
  15    ValidationTestInlineModel,
  16    ValidationTestModel,
  17)
  18
  19
  20class CheckTestCase(SimpleTestCase):
  21    def assertIsInvalid(
  22        self,
  23        model_admin,
  24        model,
  25        msg,
  26        id=None,
  27        hint=None,
  28        invalid_obj=None,
  29        admin_site=None,
  30    ):
  31        if admin_site is None:
  32            admin_site = AdminSite()
  33        invalid_obj = invalid_obj or model_admin
  34        admin_obj = model_admin(model, admin_site)
  35        self.assertEqual(
  36            admin_obj.check(), [Error(msg, hint=hint, obj=invalid_obj, id=id)]
  37        )
  38
  39    def assertIsInvalidRegexp(
  40        self, model_admin, model, msg, id=None, hint=None, invalid_obj=None
  41    ):
  42        """
  43        Same as assertIsInvalid but treats the given msg as a regexp.
  44        """
  45        invalid_obj = invalid_obj or model_admin
  46        admin_obj = model_admin(model, AdminSite())
  47        errors = admin_obj.check()
  48        self.assertEqual(len(errors), 1)
  49        error = errors[0]
  50        self.assertEqual(error.hint, hint)
  51        self.assertEqual(error.obj, invalid_obj)
  52        self.assertEqual(error.id, id)
  53        self.assertRegex(error.msg, msg)
  54
  55    def assertIsValid(self, model_admin, model, admin_site=None):
  56        if admin_site is None:
  57            admin_site = AdminSite()
  58        admin_obj = model_admin(model, admin_site)
  59        self.assertEqual(admin_obj.check(), [])
  60
  61
  62class RawIdCheckTests(CheckTestCase):
  63    def test_not_iterable(self):
  64        class TestModelAdmin(ModelAdmin):
  65            raw_id_fields = 10
  66
  67        self.assertIsInvalid(
  68            TestModelAdmin,
  69            ValidationTestModel,
  70            "The value of 'raw_id_fields' must be a list or tuple.",
  71            "admin.E001",
  72        )
  73
  74    def test_missing_field(self):
  75        class TestModelAdmin(ModelAdmin):
  76            raw_id_fields = ("non_existent_field",)
  77
  78        self.assertIsInvalid(
  79            TestModelAdmin,
  80            ValidationTestModel,
  81            "The value of 'raw_id_fields[0]' refers to 'non_existent_field', "
  82            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
  83            "admin.E002",
  84        )
  85
  86    def test_invalid_field_type(self):
  87        class TestModelAdmin(ModelAdmin):
  88            raw_id_fields = ("name",)
  89
  90        self.assertIsInvalid(
  91            TestModelAdmin,
  92            ValidationTestModel,
  93            "The value of 'raw_id_fields[0]' must be a foreign key or a "
  94            "many-to-many field.",
  95            "admin.E003",
  96        )
  97
  98    def test_valid_case(self):
  99        class TestModelAdmin(ModelAdmin):
 100            raw_id_fields = ("users",)
 101
 102        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 103
 104
 105class FieldsetsCheckTests(CheckTestCase):
 106    def test_valid_case(self):
 107        class TestModelAdmin(ModelAdmin):
 108            fieldsets = (("General", {"fields": ("name",)}),)
 109
 110        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 111
 112    def test_not_iterable(self):
 113        class TestModelAdmin(ModelAdmin):
 114            fieldsets = 10
 115
 116        self.assertIsInvalid(
 117            TestModelAdmin,
 118            ValidationTestModel,
 119            "The value of 'fieldsets' must be a list or tuple.",
 120            "admin.E007",
 121        )
 122
 123    def test_non_iterable_item(self):
 124        class TestModelAdmin(ModelAdmin):
 125            fieldsets = ({},)
 126
 127        self.assertIsInvalid(
 128            TestModelAdmin,
 129            ValidationTestModel,
 130            "The value of 'fieldsets[0]' must be a list or tuple.",
 131            "admin.E008",
 132        )
 133
 134    def test_item_not_a_pair(self):
 135        class TestModelAdmin(ModelAdmin):
 136            fieldsets = ((),)
 137
 138        self.assertIsInvalid(
 139            TestModelAdmin,
 140            ValidationTestModel,
 141            "The value of 'fieldsets[0]' must be of length 2.",
 142            "admin.E009",
 143        )
 144
 145    def test_second_element_of_item_not_a_dict(self):
 146        class TestModelAdmin(ModelAdmin):
 147            fieldsets = (("General", ()),)
 148
 149        self.assertIsInvalid(
 150            TestModelAdmin,
 151            ValidationTestModel,
 152            "The value of 'fieldsets[0][1]' must be a dictionary.",
 153            "admin.E010",
 154        )
 155
 156    def test_missing_fields_key(self):
 157        class TestModelAdmin(ModelAdmin):
 158            fieldsets = (("General", {}),)
 159
 160        self.assertIsInvalid(
 161            TestModelAdmin,
 162            ValidationTestModel,
 163            "The value of 'fieldsets[0][1]' must contain the key 'fields'.",
 164            "admin.E011",
 165        )
 166
 167        class TestModelAdmin(ModelAdmin):
 168            fieldsets = (("General", {"fields": ("name",)}),)
 169
 170        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 171
 172    def test_specified_both_fields_and_fieldsets(self):
 173        class TestModelAdmin(ModelAdmin):
 174            fieldsets = (("General", {"fields": ("name",)}),)
 175            fields = ["name"]
 176
 177        self.assertIsInvalid(
 178            TestModelAdmin,
 179            ValidationTestModel,
 180            "Both 'fieldsets' and 'fields' are specified.",
 181            "admin.E005",
 182        )
 183
 184    def test_duplicate_fields(self):
 185        class TestModelAdmin(ModelAdmin):
 186            fieldsets = [(None, {"fields": ["name", "name"]})]
 187
 188        self.assertIsInvalid(
 189            TestModelAdmin,
 190            ValidationTestModel,
 191            "There are duplicate field(s) in 'fieldsets[0][1]'.",
 192            "admin.E012",
 193        )
 194
 195    def test_duplicate_fields_in_fieldsets(self):
 196        class TestModelAdmin(ModelAdmin):
 197            fieldsets = [
 198                (None, {"fields": ["name"]}),
 199                (None, {"fields": ["name"]}),
 200            ]
 201
 202        self.assertIsInvalid(
 203            TestModelAdmin,
 204            ValidationTestModel,
 205            "There are duplicate field(s) in 'fieldsets[1][1]'.",
 206            "admin.E012",
 207        )
 208
 209    def test_fieldsets_with_custom_form_validation(self):
 210        class BandAdmin(ModelAdmin):
 211            fieldsets = (("Band", {"fields": ("name",)}),)
 212
 213        self.assertIsValid(BandAdmin, Band)
 214
 215
 216class FieldsCheckTests(CheckTestCase):
 217    def test_duplicate_fields_in_fields(self):
 218        class TestModelAdmin(ModelAdmin):
 219            fields = ["name", "name"]
 220
 221        self.assertIsInvalid(
 222            TestModelAdmin,
 223            ValidationTestModel,
 224            "The value of 'fields' contains duplicate field(s).",
 225            "admin.E006",
 226        )
 227
 228    def test_inline(self):
 229        class ValidationTestInline(TabularInline):
 230            model = ValidationTestInlineModel
 231            fields = 10
 232
 233        class TestModelAdmin(ModelAdmin):
 234            inlines = [ValidationTestInline]
 235
 236        self.assertIsInvalid(
 237            TestModelAdmin,
 238            ValidationTestModel,
 239            "The value of 'fields' must be a list or tuple.",
 240            "admin.E004",
 241            invalid_obj=ValidationTestInline,
 242        )
 243
 244
 245class FormCheckTests(CheckTestCase):
 246    def test_invalid_type(self):
 247        class FakeForm:
 248            pass
 249
 250        class TestModelAdmin(ModelAdmin):
 251            form = FakeForm
 252
 253        class TestModelAdminWithNoForm(ModelAdmin):
 254            form = "not a form"
 255
 256        for model_admin in (TestModelAdmin, TestModelAdminWithNoForm):
 257            with self.subTest(model_admin):
 258                self.assertIsInvalid(
 259                    model_admin,
 260                    ValidationTestModel,
 261                    "The value of 'form' must inherit from 'BaseModelForm'.",
 262                    "admin.E016",
 263                )
 264
 265    def test_fieldsets_with_custom_form_validation(self):
 266        class BandAdmin(ModelAdmin):
 267            fieldsets = (("Band", {"fields": ("name",)}),)
 268
 269        self.assertIsValid(BandAdmin, Band)
 270
 271    def test_valid_case(self):
 272        class AdminBandForm(forms.ModelForm):
 273            delete = forms.BooleanField()
 274
 275        class BandAdmin(ModelAdmin):
 276            form = AdminBandForm
 277            fieldsets = (("Band", {"fields": ("name", "bio", "sign_date", "delete")}),)
 278
 279        self.assertIsValid(BandAdmin, Band)
 280
 281
 282class FilterVerticalCheckTests(CheckTestCase):
 283    def test_not_iterable(self):
 284        class TestModelAdmin(ModelAdmin):
 285            filter_vertical = 10
 286
 287        self.assertIsInvalid(
 288            TestModelAdmin,
 289            ValidationTestModel,
 290            "The value of 'filter_vertical' must be a list or tuple.",
 291            "admin.E017",
 292        )
 293
 294    def test_missing_field(self):
 295        class TestModelAdmin(ModelAdmin):
 296            filter_vertical = ("non_existent_field",)
 297
 298        self.assertIsInvalid(
 299            TestModelAdmin,
 300            ValidationTestModel,
 301            "The value of 'filter_vertical[0]' refers to 'non_existent_field', "
 302            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 303            "admin.E019",
 304        )
 305
 306    def test_invalid_field_type(self):
 307        class TestModelAdmin(ModelAdmin):
 308            filter_vertical = ("name",)
 309
 310        self.assertIsInvalid(
 311            TestModelAdmin,
 312            ValidationTestModel,
 313            "The value of 'filter_vertical[0]' must be a many-to-many field.",
 314            "admin.E020",
 315        )
 316
 317    def test_valid_case(self):
 318        class TestModelAdmin(ModelAdmin):
 319            filter_vertical = ("users",)
 320
 321        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 322
 323
 324class FilterHorizontalCheckTests(CheckTestCase):
 325    def test_not_iterable(self):
 326        class TestModelAdmin(ModelAdmin):
 327            filter_horizontal = 10
 328
 329        self.assertIsInvalid(
 330            TestModelAdmin,
 331            ValidationTestModel,
 332            "The value of 'filter_horizontal' must be a list or tuple.",
 333            "admin.E018",
 334        )
 335
 336    def test_missing_field(self):
 337        class TestModelAdmin(ModelAdmin):
 338            filter_horizontal = ("non_existent_field",)
 339
 340        self.assertIsInvalid(
 341            TestModelAdmin,
 342            ValidationTestModel,
 343            "The value of 'filter_horizontal[0]' refers to 'non_existent_field', "
 344            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 345            "admin.E019",
 346        )
 347
 348    def test_invalid_field_type(self):
 349        class TestModelAdmin(ModelAdmin):
 350            filter_horizontal = ("name",)
 351
 352        self.assertIsInvalid(
 353            TestModelAdmin,
 354            ValidationTestModel,
 355            "The value of 'filter_horizontal[0]' must be a many-to-many field.",
 356            "admin.E020",
 357        )
 358
 359    def test_valid_case(self):
 360        class TestModelAdmin(ModelAdmin):
 361            filter_horizontal = ("users",)
 362
 363        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 364
 365
 366class RadioFieldsCheckTests(CheckTestCase):
 367    def test_not_dictionary(self):
 368        class TestModelAdmin(ModelAdmin):
 369            radio_fields = ()
 370
 371        self.assertIsInvalid(
 372            TestModelAdmin,
 373            ValidationTestModel,
 374            "The value of 'radio_fields' must be a dictionary.",
 375            "admin.E021",
 376        )
 377
 378    def test_missing_field(self):
 379        class TestModelAdmin(ModelAdmin):
 380            radio_fields = {"non_existent_field": VERTICAL}
 381
 382        self.assertIsInvalid(
 383            TestModelAdmin,
 384            ValidationTestModel,
 385            "The value of 'radio_fields' refers to 'non_existent_field', "
 386            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 387            "admin.E022",
 388        )
 389
 390    def test_invalid_field_type(self):
 391        class TestModelAdmin(ModelAdmin):
 392            radio_fields = {"name": VERTICAL}
 393
 394        self.assertIsInvalid(
 395            TestModelAdmin,
 396            ValidationTestModel,
 397            "The value of 'radio_fields' refers to 'name', which is not an instance "
 398            "of ForeignKey, and does not have a 'choices' definition.",
 399            "admin.E023",
 400        )
 401
 402    def test_invalid_value(self):
 403        class TestModelAdmin(ModelAdmin):
 404            radio_fields = {"state": None}
 405
 406        self.assertIsInvalid(
 407            TestModelAdmin,
 408            ValidationTestModel,
 409            "The value of 'radio_fields[\"state\"]' must be either admin.HORIZONTAL or admin.VERTICAL.",
 410            "admin.E024",
 411        )
 412
 413    def test_valid_case(self):
 414        class TestModelAdmin(ModelAdmin):
 415            radio_fields = {"state": VERTICAL}
 416
 417        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 418
 419
 420class PrepopulatedFieldsCheckTests(CheckTestCase):
 421    def test_not_list_or_tuple(self):
 422        class TestModelAdmin(ModelAdmin):
 423            prepopulated_fields = {"slug": "test"}
 424
 425        self.assertIsInvalid(
 426            TestModelAdmin,
 427            ValidationTestModel,
 428            "The value of 'prepopulated_fields[\"slug\"]' must be a list " "or tuple.",
 429            "admin.E029",
 430        )
 431
 432    def test_not_dictionary(self):
 433        class TestModelAdmin(ModelAdmin):
 434            prepopulated_fields = ()
 435
 436        self.assertIsInvalid(
 437            TestModelAdmin,
 438            ValidationTestModel,
 439            "The value of 'prepopulated_fields' must be a dictionary.",
 440            "admin.E026",
 441        )
 442
 443    def test_missing_field(self):
 444        class TestModelAdmin(ModelAdmin):
 445            prepopulated_fields = {"non_existent_field": ("slug",)}
 446
 447        self.assertIsInvalid(
 448            TestModelAdmin,
 449            ValidationTestModel,
 450            "The value of 'prepopulated_fields' refers to 'non_existent_field', "
 451            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 452            "admin.E027",
 453        )
 454
 455    def test_missing_field_again(self):
 456        class TestModelAdmin(ModelAdmin):
 457            prepopulated_fields = {"slug": ("non_existent_field",)}
 458
 459        self.assertIsInvalid(
 460            TestModelAdmin,
 461            ValidationTestModel,
 462            "The value of 'prepopulated_fields[\"slug\"][0]' refers to 'non_existent_field', "
 463            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 464            "admin.E030",
 465        )
 466
 467    def test_invalid_field_type(self):
 468        class TestModelAdmin(ModelAdmin):
 469            prepopulated_fields = {"users": ("name",)}
 470
 471        self.assertIsInvalid(
 472            TestModelAdmin,
 473            ValidationTestModel,
 474            "The value of 'prepopulated_fields' refers to 'users', which must not be "
 475            "a DateTimeField, a ForeignKey, a OneToOneField, or a ManyToManyField.",
 476            "admin.E028",
 477        )
 478
 479    def test_valid_case(self):
 480        class TestModelAdmin(ModelAdmin):
 481            prepopulated_fields = {"slug": ("name",)}
 482
 483        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 484
 485    def test_one_to_one_field(self):
 486        class TestModelAdmin(ModelAdmin):
 487            prepopulated_fields = {"best_friend": ("name",)}
 488
 489        self.assertIsInvalid(
 490            TestModelAdmin,
 491            ValidationTestModel,
 492            "The value of 'prepopulated_fields' refers to 'best_friend', which must not be "
 493            "a DateTimeField, a ForeignKey, a OneToOneField, or a ManyToManyField.",
 494            "admin.E028",
 495        )
 496
 497
 498class ListDisplayTests(CheckTestCase):
 499    def test_not_iterable(self):
 500        class TestModelAdmin(ModelAdmin):
 501            list_display = 10
 502
 503        self.assertIsInvalid(
 504            TestModelAdmin,
 505            ValidationTestModel,
 506            "The value of 'list_display' must be a list or tuple.",
 507            "admin.E107",
 508        )
 509
 510    def test_missing_field(self):
 511        class TestModelAdmin(ModelAdmin):
 512            list_display = ("non_existent_field",)
 513
 514        self.assertIsInvalid(
 515            TestModelAdmin,
 516            ValidationTestModel,
 517            "The value of 'list_display[0]' refers to 'non_existent_field', "
 518            "which is not a callable, an attribute of 'TestModelAdmin', "
 519            "or an attribute or method on 'modeladmin.ValidationTestModel'.",
 520            "admin.E108",
 521        )
 522
 523    def test_invalid_field_type(self):
 524        class TestModelAdmin(ModelAdmin):
 525            list_display = ("users",)
 526
 527        self.assertIsInvalid(
 528            TestModelAdmin,
 529            ValidationTestModel,
 530            "The value of 'list_display[0]' must not be a ManyToManyField.",
 531            "admin.E109",
 532        )
 533
 534    def test_valid_case(self):
 535        def a_callable(obj):
 536            pass
 537
 538        class TestModelAdmin(ModelAdmin):
 539            def a_method(self, obj):
 540                pass
 541
 542            list_display = ("name", "decade_published_in", "a_method", a_callable)
 543
 544        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 545
 546    def test_valid_field_accessible_via_instance(self):
 547        class PositionField(Field):
 548            """Custom field accessible only via instance."""
 549
 550            def contribute_to_class(self, cls, name):
 551                super().contribute_to_class(cls, name)
 552                setattr(cls, self.name, self)
 553
 554            def __get__(self, instance, owner):
 555                if instance is None:
 556                    raise AttributeError()
 557
 558        class TestModel(Model):
 559            field = PositionField()
 560
 561        class TestModelAdmin(ModelAdmin):
 562            list_display = ("field",)
 563
 564        self.assertIsValid(TestModelAdmin, TestModel)
 565
 566
 567class ListDisplayLinksCheckTests(CheckTestCase):
 568    def test_not_iterable(self):
 569        class TestModelAdmin(ModelAdmin):
 570            list_display_links = 10
 571
 572        self.assertIsInvalid(
 573            TestModelAdmin,
 574            ValidationTestModel,
 575            "The value of 'list_display_links' must be a list, a tuple, or None.",
 576            "admin.E110",
 577        )
 578
 579    def test_missing_field(self):
 580        class TestModelAdmin(ModelAdmin):
 581            list_display_links = ("non_existent_field",)
 582
 583        self.assertIsInvalid(
 584            TestModelAdmin,
 585            ValidationTestModel,
 586            (
 587                "The value of 'list_display_links[0]' refers to "
 588                "'non_existent_field', which is not defined in 'list_display'."
 589            ),
 590            "admin.E111",
 591        )
 592
 593    def test_missing_in_list_display(self):
 594        class TestModelAdmin(ModelAdmin):
 595            list_display_links = ("name",)
 596
 597        self.assertIsInvalid(
 598            TestModelAdmin,
 599            ValidationTestModel,
 600            "The value of 'list_display_links[0]' refers to 'name', which is not defined in 'list_display'.",
 601            "admin.E111",
 602        )
 603
 604    def test_valid_case(self):
 605        def a_callable(obj):
 606            pass
 607
 608        class TestModelAdmin(ModelAdmin):
 609            def a_method(self, obj):
 610                pass
 611
 612            list_display = ("name", "decade_published_in", "a_method", a_callable)
 613            list_display_links = ("name", "decade_published_in", "a_method", a_callable)
 614
 615        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 616
 617    def test_None_is_valid_case(self):
 618        class TestModelAdmin(ModelAdmin):
 619            list_display_links = None
 620
 621        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 622
 623    def test_list_display_links_check_skipped_if_get_list_display_overridden(self):
 624        """
 625        list_display_links check is skipped if get_list_display() is overridden.
 626        """
 627
 628        class TestModelAdmin(ModelAdmin):
 629            list_display_links = ["name", "subtitle"]
 630
 631            def get_list_display(self, request):
 632                pass
 633
 634        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 635
 636    def test_list_display_link_checked_for_list_tuple_if_get_list_display_overridden(
 637        self,
 638    ):
 639        """
 640        list_display_links is checked for list/tuple/None even if
 641        get_list_display() is overridden.
 642        """
 643
 644        class TestModelAdmin(ModelAdmin):
 645            list_display_links = "non-list/tuple"
 646
 647            def get_list_display(self, request):
 648                pass
 649
 650        self.assertIsInvalid(
 651            TestModelAdmin,
 652            ValidationTestModel,
 653            "The value of 'list_display_links' must be a list, a tuple, or None.",
 654            "admin.E110",
 655        )
 656
 657
 658class ListFilterTests(CheckTestCase):
 659    def test_list_filter_validation(self):
 660        class TestModelAdmin(ModelAdmin):
 661            list_filter = 10
 662
 663        self.assertIsInvalid(
 664            TestModelAdmin,
 665            ValidationTestModel,
 666            "The value of 'list_filter' must be a list or tuple.",
 667            "admin.E112",
 668        )
 669
 670    def test_not_list_filter_class(self):
 671        class TestModelAdmin(ModelAdmin):
 672            list_filter = ["RandomClass"]
 673
 674        self.assertIsInvalid(
 675            TestModelAdmin,
 676            ValidationTestModel,
 677            "The value of 'list_filter[0]' refers to 'RandomClass', which "
 678            "does not refer to a Field.",
 679            "admin.E116",
 680        )
 681
 682    def test_callable(self):
 683        def random_callable():
 684            pass
 685
 686        class TestModelAdmin(ModelAdmin):
 687            list_filter = [random_callable]
 688
 689        self.assertIsInvalid(
 690            TestModelAdmin,
 691            ValidationTestModel,
 692            "The value of 'list_filter[0]' must inherit from 'ListFilter'.",
 693            "admin.E113",
 694        )
 695
 696    def test_not_callable(self):
 697        class TestModelAdmin(ModelAdmin):
 698            list_filter = [[42, 42]]
 699
 700        self.assertIsInvalid(
 701            TestModelAdmin,
 702            ValidationTestModel,
 703            "The value of 'list_filter[0][1]' must inherit from 'FieldListFilter'.",
 704            "admin.E115",
 705        )
 706
 707    def test_missing_field(self):
 708        class TestModelAdmin(ModelAdmin):
 709            list_filter = ("non_existent_field",)
 710
 711        self.assertIsInvalid(
 712            TestModelAdmin,
 713            ValidationTestModel,
 714            "The value of 'list_filter[0]' refers to 'non_existent_field', "
 715            "which does not refer to a Field.",
 716            "admin.E116",
 717        )
 718
 719    def test_not_filter(self):
 720        class RandomClass:
 721            pass
 722
 723        class TestModelAdmin(ModelAdmin):
 724            list_filter = (RandomClass,)
 725
 726        self.assertIsInvalid(
 727            TestModelAdmin,
 728            ValidationTestModel,
 729            "The value of 'list_filter[0]' must inherit from 'ListFilter'.",
 730            "admin.E113",
 731        )
 732
 733    def test_not_filter_again(self):
 734        class RandomClass:
 735            pass
 736
 737        class TestModelAdmin(ModelAdmin):
 738            list_filter = (("is_active", RandomClass),)
 739
 740        self.assertIsInvalid(
 741            TestModelAdmin,
 742            ValidationTestModel,
 743            "The value of 'list_filter[0][1]' must inherit from 'FieldListFilter'.",
 744            "admin.E115",
 745        )
 746
 747    def test_not_filter_again_again(self):
 748        class AwesomeFilter(SimpleListFilter):
 749            def get_title(self):
 750                return "awesomeness"
 751
 752            def get_choices(self, request):
 753                return (("bit", "A bit awesome"), ("very", "Very awesome"))
 754
 755            def get_queryset(self, cl, qs):
 756                return qs
 757
 758        class TestModelAdmin(ModelAdmin):
 759            list_filter = (("is_active", AwesomeFilter),)
 760
 761        self.assertIsInvalid(
 762            TestModelAdmin,
 763            ValidationTestModel,
 764            "The value of 'list_filter[0][1]' must inherit from 'FieldListFilter'.",
 765            "admin.E115",
 766        )
 767
 768    def test_list_filter_is_func(self):
 769        def get_filter():
 770            pass
 771
 772        class TestModelAdmin(ModelAdmin):
 773            list_filter = [get_filter]
 774
 775        self.assertIsInvalid(
 776            TestModelAdmin,
 777            ValidationTestModel,
 778            "The value of 'list_filter[0]' must inherit from 'ListFilter'.",
 779            "admin.E113",
 780        )
 781
 782    def test_not_associated_with_field_name(self):
 783        class TestModelAdmin(ModelAdmin):
 784            list_filter = (BooleanFieldListFilter,)
 785
 786        self.assertIsInvalid(
 787            TestModelAdmin,
 788            ValidationTestModel,
 789            "The value of 'list_filter[0]' must not inherit from 'FieldListFilter'.",
 790            "admin.E114",
 791        )
 792
 793    def test_valid_case(self):
 794        class AwesomeFilter(SimpleListFilter):
 795            def get_title(self):
 796                return "awesomeness"
 797
 798            def get_choices(self, request):
 799                return (("bit", "A bit awesome"), ("very", "Very awesome"))
 800
 801            def get_queryset(self, cl, qs):
 802                return qs
 803
 804        class TestModelAdmin(ModelAdmin):
 805            list_filter = (
 806                "is_active",
 807                AwesomeFilter,
 808                ("is_active", BooleanFieldListFilter),
 809                "no",
 810            )
 811
 812        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 813
 814
 815class ListPerPageCheckTests(CheckTestCase):
 816    def test_not_integer(self):
 817        class TestModelAdmin(ModelAdmin):
 818            list_per_page = "hello"
 819
 820        self.assertIsInvalid(
 821            TestModelAdmin,
 822            ValidationTestModel,
 823            "The value of 'list_per_page' must be an integer.",
 824            "admin.E118",
 825        )
 826
 827    def test_valid_case(self):
 828        class TestModelAdmin(ModelAdmin):
 829            list_per_page = 100
 830
 831        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 832
 833
 834class ListMaxShowAllCheckTests(CheckTestCase):
 835    def test_not_integer(self):
 836        class TestModelAdmin(ModelAdmin):
 837            list_max_show_all = "hello"
 838
 839        self.assertIsInvalid(
 840            TestModelAdmin,
 841            ValidationTestModel,
 842            "The value of 'list_max_show_all' must be an integer.",
 843            "admin.E119",
 844        )
 845
 846    def test_valid_case(self):
 847        class TestModelAdmin(ModelAdmin):
 848            list_max_show_all = 200
 849
 850        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 851
 852
 853class SearchFieldsCheckTests(CheckTestCase):
 854    def test_not_iterable(self):
 855        class TestModelAdmin(ModelAdmin):
 856            search_fields = 10
 857
 858        self.assertIsInvalid(
 859            TestModelAdmin,
 860            ValidationTestModel,
 861            "The value of 'search_fields' must be a list or tuple.",
 862            "admin.E126",
 863        )
 864
 865
 866class DateHierarchyCheckTests(CheckTestCase):
 867    def test_missing_field(self):
 868        class TestModelAdmin(ModelAdmin):
 869            date_hierarchy = "non_existent_field"
 870
 871        self.assertIsInvalid(
 872            TestModelAdmin,
 873            ValidationTestModel,
 874            "The value of 'date_hierarchy' refers to 'non_existent_field', "
 875            "which does not refer to a Field.",
 876            "admin.E127",
 877        )
 878
 879    def test_invalid_field_type(self):
 880        class TestModelAdmin(ModelAdmin):
 881            date_hierarchy = "name"
 882
 883        self.assertIsInvalid(
 884            TestModelAdmin,
 885            ValidationTestModel,
 886            "The value of 'date_hierarchy' must be a DateField or DateTimeField.",
 887            "admin.E128",
 888        )
 889
 890    def test_valid_case(self):
 891        class TestModelAdmin(ModelAdmin):
 892            date_hierarchy = "pub_date"
 893
 894        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 895
 896    def test_related_valid_case(self):
 897        class TestModelAdmin(ModelAdmin):
 898            date_hierarchy = "band__sign_date"
 899
 900        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 901
 902    def test_related_invalid_field_type(self):
 903        class TestModelAdmin(ModelAdmin):
 904            date_hierarchy = "band__name"
 905
 906        self.assertIsInvalid(
 907            TestModelAdmin,
 908            ValidationTestModel,
 909            "The value of 'date_hierarchy' must be a DateField or DateTimeField.",
 910            "admin.E128",
 911        )
 912
 913
 914class OrderingCheckTests(CheckTestCase):
 915    def test_not_iterable(self):
 916        class TestModelAdmin(ModelAdmin):
 917            ordering = 10
 918
 919        self.assertIsInvalid(
 920            TestModelAdmin,
 921            ValidationTestModel,
 922            "The value of 'ordering' must be a list or tuple.",
 923            "admin.E031",
 924        )
 925
 926        class TestModelAdmin(ModelAdmin):
 927            ordering = ("non_existent_field",)
 928
 929        self.assertIsInvalid(
 930            TestModelAdmin,
 931            ValidationTestModel,
 932            "The value of 'ordering[0]' refers to 'non_existent_field', "
 933            "which is not an attribute of 'modeladmin.ValidationTestModel'.",
 934            "admin.E033",
 935        )
 936
 937    def test_random_marker_not_alone(self):
 938        class TestModelAdmin(ModelAdmin):
 939            ordering = ("?", "name")
 940
 941        self.assertIsInvalid(
 942            TestModelAdmin,
 943            ValidationTestModel,
 944            "The value of 'ordering' has the random ordering marker '?', but contains "
 945            "other fields as well.",
 946            "admin.E032",
 947            hint='Either remove the "?", or remove the other fields.',
 948        )
 949
 950    def test_valid_random_marker_case(self):
 951        class TestModelAdmin(ModelAdmin):
 952            ordering = ("?",)
 953
 954        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 955
 956    def test_valid_complex_case(self):
 957        class TestModelAdmin(ModelAdmin):
 958            ordering = ("band__name",)
 959
 960        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 961
 962    def test_valid_case(self):
 963        class TestModelAdmin(ModelAdmin):
 964            ordering = ("name", "pk")
 965
 966        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 967
 968    def test_invalid_expression(self):
 969        class TestModelAdmin(ModelAdmin):
 970            ordering = (F("nonexistent"),)
 971
 972        self.assertIsInvalid(
 973            TestModelAdmin,
 974            ValidationTestModel,
 975            "The value of 'ordering[0]' refers to 'nonexistent', which is not "
 976            "an attribute of 'modeladmin.ValidationTestModel'.",
 977            "admin.E033",
 978        )
 979
 980    def test_valid_expression(self):
 981        class TestModelAdmin(ModelAdmin):
 982            ordering = (Upper("name"), Upper("band__name").desc())
 983
 984        self.assertIsValid(TestModelAdmin, ValidationTestModel)
 985
 986
 987class ListSelectRelatedCheckTests(CheckTestCase):
 988    def test_invalid_type(self):
 989        class TestModelAdmin(ModelAdmin):
 990            list_select_related = 1
 991
 992        self.assertIsInvalid(
 993            TestModelAdmin,
 994            ValidationTestModel,
 995            "The value of 'list_select_related' must be a boolean, tuple or list.",
 996            "admin.E117",
 997        )
 998
 999    def test_valid_case(self):
1000        class TestModelAdmin(ModelAdmin):
1001            list_select_related = False
1002
1003        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1004
1005
1006class SaveAsCheckTests(CheckTestCase):
1007    def test_not_boolean(self):
1008        class TestModelAdmin(ModelAdmin):
1009            save_as = 1
1010
1011        self.assertIsInvalid(
1012            TestModelAdmin,
1013            ValidationTestModel,
1014            "The value of 'save_as' must be a boolean.",
1015            "admin.E101",
1016        )
1017
1018    def test_valid_case(self):
1019        class TestModelAdmin(ModelAdmin):
1020            save_as = True
1021
1022        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1023
1024
1025class SaveOnTopCheckTests(CheckTestCase):
1026    def test_not_boolean(self):
1027        class TestModelAdmin(ModelAdmin):
1028            save_on_top = 1
1029
1030        self.assertIsInvalid(
1031            TestModelAdmin,
1032            ValidationTestModel,
1033            "The value of 'save_on_top' must be a boolean.",
1034            "admin.E102",
1035        )
1036
1037    def test_valid_case(self):
1038        class TestModelAdmin(ModelAdmin):
1039            save_on_top = True
1040
1041        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1042
1043
1044class InlinesCheckTests(CheckTestCase):
1045    def test_not_iterable(self):
1046        class TestModelAdmin(ModelAdmin):
1047            inlines = 10
1048
1049        self.assertIsInvalid(
1050            TestModelAdmin,
1051            ValidationTestModel,
1052            "The value of 'inlines' must be a list or tuple.",
1053            "admin.E103",
1054        )
1055
1056    def test_not_correct_inline_field(self):
1057        class TestModelAdmin(ModelAdmin):
1058            inlines = [42]
1059
1060        self.assertIsInvalidRegexp(
1061            TestModelAdmin,
1062            ValidationTestModel,
1063            r"'.*\.TestModelAdmin' must inherit from 'InlineModelAdmin'\.",
1064            "admin.E104",
1065        )
1066
1067    def test_not_model_admin(self):
1068        class ValidationTestInline:
1069            pass
1070
1071        class TestModelAdmin(ModelAdmin):
1072            inlines = [ValidationTestInline]
1073
1074        self.assertIsInvalidRegexp(
1075            TestModelAdmin,
1076            ValidationTestModel,
1077            r"'.*\.ValidationTestInline' must inherit from 'InlineModelAdmin'\.",
1078            "admin.E104",
1079        )
1080
1081    def test_missing_model_field(self):
1082        class ValidationTestInline(TabularInline):
1083            pass
1084
1085        class TestModelAdmin(ModelAdmin):
1086            inlines = [ValidationTestInline]
1087
1088        self.assertIsInvalidRegexp(
1089            TestModelAdmin,
1090            ValidationTestModel,
1091            r"'.*\.ValidationTestInline' must have a 'model' attribute\.",
1092            "admin.E105",
1093        )
1094
1095    def test_invalid_model_type(self):
1096        class SomethingBad:
1097            pass
1098
1099        class ValidationTestInline(TabularInline):
1100            model = SomethingBad
1101
1102        class TestModelAdmin(ModelAdmin):
1103            inlines = [ValidationTestInline]
1104
1105        self.assertIsInvalidRegexp(
1106            TestModelAdmin,
1107            ValidationTestModel,
1108            r"The value of '.*\.ValidationTestInline.model' must be a Model\.",
1109            "admin.E106",
1110        )
1111
1112    def test_invalid_model(self):
1113        class ValidationTestInline(TabularInline):
1114            model = "Not a class"
1115
1116        class TestModelAdmin(ModelAdmin):
1117            inlines = [ValidationTestInline]
1118
1119        self.assertIsInvalidRegexp(
1120            TestModelAdmin,
1121            ValidationTestModel,
1122            r"The value of '.*\.ValidationTestInline.model' must be a Model\.",
1123            "admin.E106",
1124        )
1125
1126    def test_invalid_callable(self):
1127        def random_obj():
1128            pass
1129
1130        class TestModelAdmin(ModelAdmin):
1131            inlines = [random_obj]
1132
1133        self.assertIsInvalidRegexp(
1134            TestModelAdmin,
1135            ValidationTestModel,
1136            r"'.*\.random_obj' must inherit from 'InlineModelAdmin'\.",
1137            "admin.E104",
1138        )
1139
1140    def test_valid_case(self):
1141        class ValidationTestInline(TabularInline):
1142            model = ValidationTestInlineModel
1143
1144        class TestModelAdmin(ModelAdmin):
1145            inlines = [ValidationTestInline]
1146
1147        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1148
1149
1150class FkNameCheckTests(CheckTestCase):
1151    def test_missing_field(self):
1152        class ValidationTestInline(TabularInline):
1153            model = ValidationTestInlineModel
1154            fk_name = "non_existent_field"
1155
1156        class TestModelAdmin(ModelAdmin):
1157            inlines = [ValidationTestInline]
1158
1159        self.assertIsInvalid(
1160            TestModelAdmin,
1161            ValidationTestModel,
1162            "'modeladmin.ValidationTestInlineModel' has no field named 'non_existent_field'.",
1163            "admin.E202",
1164            invalid_obj=ValidationTestInline,
1165        )
1166
1167    def test_valid_case(self):
1168        class ValidationTestInline(TabularInline):
1169            model = ValidationTestInlineModel
1170            fk_name = "parent"
1171
1172        class TestModelAdmin(ModelAdmin):
1173            inlines = [ValidationTestInline]
1174
1175        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1176
1177
1178class ExtraCheckTests(CheckTestCase):
1179    def test_not_integer(self):
1180        class ValidationTestInline(TabularInline):
1181            model = ValidationTestInlineModel
1182            extra = "hello"
1183
1184        class TestModelAdmin(ModelAdmin):
1185            inlines = [ValidationTestInline]
1186
1187        self.assertIsInvalid(
1188            TestModelAdmin,
1189            ValidationTestModel,
1190            "The value of 'extra' must be an integer.",
1191            "admin.E203",
1192            invalid_obj=ValidationTestInline,
1193        )
1194
1195    def test_valid_case(self):
1196        class ValidationTestInline(TabularInline):
1197            model = ValidationTestInlineModel
1198            extra = 2
1199
1200        class TestModelAdmin(ModelAdmin):
1201            inlines = [ValidationTestInline]
1202
1203        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1204
1205
1206class MaxNumCheckTests(CheckTestCase):
1207    def test_not_integer(self):
1208        class ValidationTestInline(TabularInline):
1209            model = ValidationTestInlineModel
1210            max_num = "hello"
1211
1212        class TestModelAdmin(ModelAdmin):
1213            inlines = [ValidationTestInline]
1214
1215        self.assertIsInvalid(
1216            TestModelAdmin,
1217            ValidationTestModel,
1218            "The value of 'max_num' must be an integer.",
1219            "admin.E204",
1220            invalid_obj=ValidationTestInline,
1221        )
1222
1223    def test_valid_case(self):
1224        class ValidationTestInline(TabularInline):
1225            model = ValidationTestInlineModel
1226            max_num = 2
1227
1228        class TestModelAdmin(ModelAdmin):
1229            inlines = [ValidationTestInline]
1230
1231        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1232
1233
1234class MinNumCheckTests(CheckTestCase):
1235    def test_not_integer(self):
1236        class ValidationTestInline(TabularInline):
1237            model = ValidationTestInlineModel
1238            min_num = "hello"
1239
1240        class TestModelAdmin(ModelAdmin):
1241            inlines = [ValidationTestInline]
1242
1243        self.assertIsInvalid(
1244            TestModelAdmin,
1245            ValidationTestModel,
1246            "The value of 'min_num' must be an integer.",
1247            "admin.E205",
1248            invalid_obj=ValidationTestInline,
1249        )
1250
1251    def test_valid_case(self):
1252        class ValidationTestInline(TabularInline):
1253            model = ValidationTestInlineModel
1254            min_num = 2
1255
1256        class TestModelAdmin(ModelAdmin):
1257            inlines = [ValidationTestInline]
1258
1259        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1260
1261
1262class FormsetCheckTests(CheckTestCase):
1263    def test_invalid_type(self):
1264        class FakeFormSet:
1265            pass
1266
1267        class ValidationTestInline(TabularInline):
1268            model = ValidationTestInlineModel
1269            formset = FakeFormSet
1270
1271        class TestModelAdmin(ModelAdmin):
1272            inlines = [ValidationTestInline]
1273
1274        self.assertIsInvalid(
1275            TestModelAdmin,
1276            ValidationTestModel,
1277            "The value of 'formset' must inherit from 'BaseModelFormSet'.",
1278            "admin.E206",
1279            invalid_obj=ValidationTestInline,
1280        )
1281
1282    def test_inline_without_formset_class(self):
1283        class ValidationTestInlineWithoutFormsetClass(TabularInline):
1284            model = ValidationTestInlineModel
1285            formset = "Not a FormSet Class"
1286
1287        class TestModelAdminWithoutFormsetClass(ModelAdmin):
1288            inlines = [ValidationTestInlineWithoutFormsetClass]
1289
1290        self.assertIsInvalid(
1291            TestModelAdminWithoutFormsetClass,
1292            ValidationTestModel,
1293            "The value of 'formset' must inherit from 'BaseModelFormSet'.",
1294            "admin.E206",
1295            invalid_obj=ValidationTestInlineWithoutFormsetClass,
1296        )
1297
1298    def test_valid_case(self):
1299        class RealModelFormSet(BaseModelFormSet):
1300            pass
1301
1302        class ValidationTestInline(TabularInline):
1303            model = ValidationTestInlineModel
1304            formset = RealModelFormSet
1305
1306        class TestModelAdmin(ModelAdmin):
1307            inlines = [ValidationTestInline]
1308
1309        self.assertIsValid(TestModelAdmin, ValidationTestModel)
1310
1311
1312class ListDisplayEditableTests(CheckTestCase):
1313    def test_list_display_links_is_none(self):
1314        """
1315        list_display and list_editable can contain the same values
1316        when list_display_links is None
1317        """
1318
1319        class ProductAdmin(ModelAdmin):
1320            list_display = ["name", "slug", "pub_date"]
1321            list_editable = list_display
1322            list_display_links = None
1323
1324        self.assertIsValid(ProductAdmin, ValidationTestModel)
1325
1326    def test_list_display_first_item_same_as_list_editable_first_item(self):
1327        """
1328        The first item in list_display can be the same as the first in
1329        list_editable.
1330        """
1331
1332        class ProductAdmin(ModelAdmin):
1333            list_display = ["name", "slug", "pub_date"]
1334            list_editable = ["name", "slug"]
1335            list_display_links = ["pub_date"]
1336
1337        self.assertIsValid(ProductAdmin, ValidationTestModel)
1338
1339    def test_list_display_first_item_in_list_editable(self):
1340        """
1341        The first item in list_display can be in list_editable as long as
1342        list_display_links is defined.
1343        """
1344
1345        class ProductAdmin(ModelAdmin):
1346            list_display = ["name", "slug", "pub_date"]
1347            list_editable = ["slug", "name"]
1348            list_display_links = ["pub_date"]
1349
1350        self.assertIsValid(ProductAdmin, ValidationTestModel)
1351
1352    def test_list_display_first_item_same_as_list_editable_no_list_display_links(self):
1353        """
1354        The first item in list_display cannot be the same as the first item
1355        in list_editable if list_display_links is not defined.
1356        """
1357
1358        class ProductAdmin(ModelAdmin):
1359            list_display = ["name"]
1360            list_editable = ["name"]
1361
1362        self.assertIsInvalid(
1363            ProductAdmin,
1364            ValidationTestModel,
1365            "The value of 'list_editable[0]' refers to the first field "
1366            "in 'list_display' ('name'), which cannot be used unless "
1367            "'list_display_links' is set.",
1368            id="admin.E124",
1369        )
1370
1371    def test_list_display_first_item_in_list_editable_no_list_display_links(self):
1372        """
1373        The first item in list_display cannot be in list_editable if
1374        list_display_links isn't defined.
1375        """
1376
1377        class ProductAdmin(ModelAdmin):
1378            list_display = ["name", "slug", "pub_date"]
1379            list_editable = ["slug", "name"]
1380
1381        self.assertIsInvalid(
1382            ProductAdmin,
1383            ValidationTestModel,
1384            "The value of 'list_editable[1]' refers to the first field "
1385            "in 'list_display' ('name'), which cannot be used unless "
1386            "'list_display_links' is set.",
1387            id="admin.E124",
1388        )
1389
1390    def test_both_list_editable_and_list_display_links(self):
1391        class ProductAdmin(ModelAdmin):
1392            list_editable = ("name",)
1393            list_display = ("name",)
1394            list_display_links = ("name",)
1395
1396        self.assertIsInvalid(
1397            ProductAdmin,
1398            ValidationTestModel,
1399            "The value of 'name' cannot be in both 'list_editable' and "
1400            "'list_display_links'.",
1401            id="admin.E123",
1402        )
1403
1404
1405class AutocompleteFieldsTests(CheckTestCase):
1406    def test_autocomplete_e036(self):
1407        class Admin(ModelAdmin):
1408            autocomplete_fields = "name"
1409
1410        self.assertIsInvalid(
1411            Admin,
1412            Band,
1413            msg="The value of 'autocomplete_fields' must be a list or tuple.",
1414            id="admin.E036",
1415            invalid_obj=Admin,
1416        )
1417
1418    def test_autocomplete_e037(self):
1419        class Admin(ModelAdmin):
1420            autocomplete_fields = ("nonexistent",)
1421
1422        self.assertIsInvalid(
1423            Admin,
1424            ValidationTestModel,
1425            msg=(
1426                "The value of 'autocomplete_fields[0]' refers to 'nonexistent', "
1427                "which is not an attribute of 'modeladmin.ValidationTestModel'."
1428            ),
1429            id="admin.E037",
1430            invalid_obj=Admin,
1431        )
1432
1433    def test_autocomplete_e38(self):
1434        class Admin(ModelAdmin):
1435            autocomplete_fields = ("name",)
1436
1437        self.assertIsInvalid(
1438            Admin,
1439            ValidationTestModel,
1440            msg=(
1441                "The value of 'autocomplete_fields[0]' must be a foreign "
1442                "key or a many-to-many field."
1443            ),
1444            id="admin.E038",
1445            invalid_obj=Admin,
1446        )
1447
1448    def test_autocomplete_e039(self):
1449        class Admin(ModelAdmin):
1450            autocomplete_fields = ("band",)
1451
1452        self.assertIsInvalid(
1453            Admin,
1454            Song,
1455            msg=(
1456                'An admin for model "Band" has to be registered '
1457                "to be referenced by Admin.autocomplete_fields."
1458            ),
1459            id="admin.E039",
1460            invalid_obj=Admin,
1461        )
1462
1463    def test_autocomplete_e040(self):
1464        class NoSearchFieldsAdmin(ModelAdmin):
1465            pass
1466
1467        class AutocompleteAdmin(ModelAdmin):
1468            autocomplete_fields = ("featuring",)
1469
1470        site = AdminSite()
1471        site.register(Band, NoSearchFieldsAdmin)
1472        self.assertIsInvalid(
1473            AutocompleteAdmin,
1474            Song,
1475            msg=(
1476                'NoSearchFieldsAdmin must define "search_fields", because '
1477                "it's referenced by AutocompleteAdmin.autocomplete_fields."
1478            ),
1479            id="admin.E040",
1480            invalid_obj=AutocompleteAdmin,
1481            admin_site=site,
1482        )
1483
1484    def test_autocomplete_is_valid(self):
1485        class SearchFieldsAdmin(ModelAdmin):
1486            search_fields = "name"
1487
1488        class AutocompleteAdmin(ModelAdmin):
1489            autocomplete_fields = ("featuring",)
1490
1491        site = AdminSite()
1492        site.register(Band, SearchFieldsAdmin)
1493        self.assertIsValid(AutocompleteAdmin, Song, admin_site=site)
1494
1495    def test_autocomplete_is_onetoone(self):
1496        class UserAdmin(ModelAdmin):
1497            search_fields = ("name",)
1498
1499        class Admin(ModelAdmin):
1500            autocomplete_fields = ("best_friend",)
1501
1502        site = AdminSite()
1503        site.register(User, UserAdmin)
1504        self.assertIsValid(Admin, ValidationTestModel, admin_site=site)
1505
1506
1507class ActionsCheckTests(CheckTestCase):
1508    def test_custom_permissions_require_matching_has_method(self):
1509        def custom_permission_action(modeladmin, request, queryset):
1510            pass
1511
1512        custom_permission_action.allowed_permissions = ("custom",)
1513
1514        class BandAdmin(ModelAdmin):
1515            actions = (custom_permission_action,)
1516
1517        self.assertIsInvalid(
1518            BandAdmin,
1519            Band,
1520            "BandAdmin must define a has_custom_permission() method for the "
1521            "custom_permission_action action.",
1522            id="admin.E129",
1523        )
1524
1525    def test_actions_not_unique(self):
1526        def action(modeladmin, request, queryset):
1527            pass
1528
1529        class BandAdmin(ModelAdmin):
1530            actions = (action, action)
1531
1532        self.assertIsInvalid(
1533            BandAdmin,
1534            Band,
1535            "__name__ attributes of actions defined in "
1536            "<class 'modeladmin.test_checks.ActionsCheckTests."
1537            "test_actions_not_unique.<locals>.BandAdmin'> must be unique.",
1538            id="admin.E130",
1539        )
1540
1541    def test_actions_unique(self):
1542        def action1(modeladmin, request, queryset):
1543            pass
1544
1545        def action2(modeladmin, request, queryset):
1546            pass
1547
1548        class BandAdmin(ModelAdmin):
1549            actions = (action1, action2)
1550
1551        self.assertIsValid(BandAdmin, Band)

tests/modeladmin/tests.py

  1from datetime import date
  2
  3from django import forms
  4from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry
  5from django.contrib.admin.options import (
  6    HORIZONTAL,
  7    VERTICAL,
  8    ModelAdmin,
  9    TabularInline,
 10    get_content_type_for_model,
 11)
 12from django.contrib.admin.sites import AdminSite
 13from django.contrib.admin.widgets import (
 14    AdminDateWidget,
 15    AdminRadioSelect,
 16    AutocompleteSelect,
 17    AutocompleteSelectMultiple,
 18)
 19from django.contrib.auth.models import User
 20from django.db import models
 21from django.forms.widgets import Select
 22from django.test import SimpleTestCase, TestCase
 23from django.test.utils import isolate_apps
 24
 25from .models import Band, Concert, Song
 26
 27
 28class MockRequest:
 29    pass
 30
 31
 32class MockSuperUser:
 33    def has_perm(self, perm):
 34        return True
 35
 36
 37request = MockRequest()
 38request.user = MockSuperUser()
 39
 40
 41class ModelAdminTests(TestCase):
 42    def setUp(self):
 43        self.band = Band.objects.create(
 44            name="The Doors", bio="", sign_date=date(1965, 1, 1),
 45        )
 46        self.site = AdminSite()
 47
 48    def test_modeladmin_str(self):
 49        ma = ModelAdmin(Band, self.site)
 50        self.assertEqual(str(ma), "modeladmin.ModelAdmin")
 51
 52    # form/fields/fieldsets interaction ##############################
 53
 54    def test_default_fields(self):
 55        ma = ModelAdmin(Band, self.site)
 56        self.assertEqual(
 57            list(ma.get_form(request).base_fields), ["name", "bio", "sign_date"]
 58        )
 59        self.assertEqual(list(ma.get_fields(request)), ["name", "bio", "sign_date"])
 60        self.assertEqual(
 61            list(ma.get_fields(request, self.band)), ["name", "bio", "sign_date"]
 62        )
 63        self.assertIsNone(ma.get_exclude(request, self.band))
 64
 65    def test_default_fieldsets(self):
 66        # fieldsets_add and fieldsets_change should return a special data structure that
 67        # is used in the templates. They should generate the "right thing" whether we
 68        # have specified a custom form, the fields argument, or nothing at all.
 69        #
 70        # Here's the default case. There are no custom form_add/form_change methods,
 71        # no fields argument, and no fieldsets argument.
 72        ma = ModelAdmin(Band, self.site)
 73        self.assertEqual(
 74            ma.get_fieldsets(request),
 75            [(None, {"fields": ["name", "bio", "sign_date"]})],
 76        )
 77        self.assertEqual(
 78            ma.get_fieldsets(request, self.band),
 79            [(None, {"fields": ["name", "bio", "sign_date"]})],
 80        )
 81
 82    def test_get_fieldsets(self):
 83        # get_fieldsets() is called when figuring out form fields (#18681).
 84        class BandAdmin(ModelAdmin):
 85            def get_fieldsets(self, request, obj=None):
 86                return [(None, {"fields": ["name", "bio"]})]
 87
 88        ma = BandAdmin(Band, self.site)
 89        form = ma.get_form(None)
 90        self.assertEqual(form._meta.fields, ["name", "bio"])
 91
 92        class InlineBandAdmin(TabularInline):
 93            model = Concert
 94            fk_name = "main_band"
 95            can_delete = False
 96
 97            def get_fieldsets(self, request, obj=None):
 98                return [(None, {"fields": ["day", "transport"]})]
 99
100        ma = InlineBandAdmin(Band, self.site)
101        form = ma.get_formset(None).form
102        self.assertEqual(form._meta.fields, ["day", "transport"])
103
104    def test_lookup_allowed_allows_nonexistent_lookup(self):
105        """
106        A lookup_allowed allows a parameter whose field lookup doesn't exist.
107        (#21129).
108        """
109
110        class BandAdmin(ModelAdmin):
111            fields = ["name"]
112
113        ma = BandAdmin(Band, self.site)
114        self.assertTrue(ma.lookup_allowed("name__nonexistent", "test_value"))
115
116    @isolate_apps("modeladmin")
117    def test_lookup_allowed_onetoone(self):
118        class Department(models.Model):
119            code = models.CharField(max_length=4, unique=True)
120
121        class Employee(models.Model):
122            department = models.ForeignKey(Department, models.CASCADE, to_field="code")
123
124        class EmployeeProfile(models.Model):
125            employee = models.OneToOneField(Employee, models.CASCADE)
126
127        class EmployeeInfo(models.Model):
128            employee = models.OneToOneField(Employee, models.CASCADE)
129            description = models.CharField(max_length=100)
130
131        class EmployeeProfileAdmin(ModelAdmin):
132            list_filter = [
133                "employee__employeeinfo__description",
134                "employee__department__code",
135            ]
136
137        ma = EmployeeProfileAdmin(EmployeeProfile, self.site)
138        # Reverse OneToOneField
139        self.assertIs(
140            ma.lookup_allowed("employee__employeeinfo__description", "test_value"), True
141        )
142        # OneToOneField and ForeignKey
143        self.assertIs(
144            ma.lookup_allowed("employee__department__code", "test_value"), True
145        )
146
147    def test_field_arguments(self):
148        # If fields is specified, fieldsets_add and fieldsets_change should
149        # just stick the fields into a formsets structure and return it.
150        class BandAdmin(ModelAdmin):
151            fields = ["name"]
152
153        ma = BandAdmin(Band, self.site)
154
155        self.assertEqual(list(ma.get_fields(request)), ["name"])
156        self.assertEqual(list(ma.get_fields(request, self.band)), ["name"])
157        self.assertEqual(ma.get_fieldsets(request), [(None, {"fields": ["name"]})])
158        self.assertEqual(
159            ma.get_fieldsets(request, self.band), [(None, {"fields": ["name"]})]
160        )
161
162    def test_field_arguments_restricted_on_form(self):
163        # If fields or fieldsets is specified, it should exclude fields on the
164        # Form class to the fields specified. This may cause errors to be
165        # raised in the db layer if required model fields aren't in fields/
166        # fieldsets, but that's preferable to ghost errors where a field in the
167        # Form class isn't being displayed because it's not in fields/fieldsets.
168
169        # Using `fields`.
170        class BandAdmin(ModelAdmin):
171            fields = ["name"]
172
173        ma = BandAdmin(Band, self.site)
174        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
175        self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
176
177        # Using `fieldsets`.
178        class BandAdmin(ModelAdmin):
179            fieldsets = [(None, {"fields": ["name"]})]
180
181        ma = BandAdmin(Band, self.site)
182        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
183        self.assertEqual(list(ma.get_form(request, self.band).base_fields), ["name"])
184
185        # Using `exclude`.
186        class BandAdmin(ModelAdmin):
187            exclude = ["bio"]
188
189        ma = BandAdmin(Band, self.site)
190        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
191
192        # You can also pass a tuple to `exclude`.
193        class BandAdmin(ModelAdmin):
194            exclude = ("bio",)
195
196        ma = BandAdmin(Band, self.site)
197        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
198
199        # Using `fields` and `exclude`.
200        class BandAdmin(ModelAdmin):
201            fields = ["name", "bio"]
202            exclude = ["bio"]
203
204        ma = BandAdmin(Band, self.site)
205        self.assertEqual(list(ma.get_form(request).base_fields), ["name"])
206
207    def test_custom_form_meta_exclude_with_readonly(self):
208        """
209        The custom ModelForm's `Meta.exclude` is respected when used in
210        conjunction with `ModelAdmin.readonly_fields` and when no
211        `ModelAdmin.exclude` is defined (#14496).
212        """
213        # With ModelAdmin
214        class AdminBandForm(forms.ModelForm):
215            class Meta:
216                model = Band
217                exclude = ["bio"]
218
219        class BandAdmin(ModelAdmin):
220            readonly_fields = ["name"]
221            form = AdminBandForm
222
223        ma = BandAdmin(Band, self.site)
224        self.assertEqual(list(ma.get_form(request).base_fields), ["sign_date"])
225
226        # With InlineModelAdmin
227        class AdminConcertForm(forms.ModelForm):
228            class Meta:
229                model = Concert
230                exclude = ["day"]
231
232        class ConcertInline(TabularInline):
233            readonly_fields = ["transport"]
234            form = AdminConcertForm
235            fk_name = "main_band"
236            model = Concert
237
238        class BandAdmin(ModelAdmin):
239            inlines = [ConcertInline]
240
241        ma = BandAdmin(Band, self.site)
242        self.assertEqual(
243            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
244            ["main_band", "opening_band", "id", "DELETE"],
245        )
246
247    def test_custom_formfield_override_readonly(self):
248        class AdminBandForm(forms.ModelForm):
249            name = forms.CharField()
250
251            class Meta:
252                exclude = ()
253                model = Band
254
255        class BandAdmin(ModelAdmin):
256            form = AdminBandForm
257            readonly_fields = ["name"]
258
259        ma = BandAdmin(Band, self.site)
260
261        # `name` shouldn't appear in base_fields because it's part of
262        # readonly_fields.
263        self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
264        # But it should appear in get_fields()/fieldsets() so it can be
265        # displayed as read-only.
266        self.assertEqual(list(ma.get_fields(request)), ["bio", "sign_date", "name"])
267        self.assertEqual(
268            list(ma.get_fieldsets(request)),
269            [(None, {"fields": ["bio", "sign_date", "name"]})],
270        )
271
272    def test_custom_form_meta_exclude(self):
273        """
274        The custom ModelForm's `Meta.exclude` is overridden if
275        `ModelAdmin.exclude` or `InlineModelAdmin.exclude` are defined (#14496).
276        """
277        # With ModelAdmin
278        class AdminBandForm(forms.ModelForm):
279            class Meta:
280                model = Band
281                exclude = ["bio"]
282
283        class BandAdmin(ModelAdmin):
284            exclude = ["name"]
285            form = AdminBandForm
286
287        ma = BandAdmin(Band, self.site)
288        self.assertEqual(list(ma.get_form(request).base_fields), ["bio", "sign_date"])
289
290        # With InlineModelAdmin
291        class AdminConcertForm(forms.ModelForm):
292            class Meta:
293                model = Concert
294                exclude = ["day"]
295
296        class ConcertInline(TabularInline):
297            exclude = ["transport"]
298            form = AdminConcertForm
299            fk_name = "main_band"
300            model = Concert
301
302        class BandAdmin(ModelAdmin):
303            inlines = [ConcertInline]
304
305        ma = BandAdmin(Band, self.site)
306        self.assertEqual(
307            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
308            ["main_band", "opening_band", "day", "id", "DELETE"],
309        )
310
311    def test_overriding_get_exclude(self):
312        class BandAdmin(ModelAdmin):
313            def get_exclude(self, request, obj=None):
314                return ["name"]
315
316        self.assertEqual(
317            list(BandAdmin(Band, self.site).get_form(request).base_fields),
318            ["bio", "sign_date"],
319        )
320
321    def test_get_exclude_overrides_exclude(self):
322        class BandAdmin(ModelAdmin):
323            exclude = ["bio"]
324
325            def get_exclude(self, request, obj=None):
326                return ["name"]
327
328        self.assertEqual(
329            list(BandAdmin(Band, self.site).get_form(request).base_fields),
330            ["bio", "sign_date"],
331        )
332
333    def test_get_exclude_takes_obj(self):
334        class BandAdmin(ModelAdmin):
335            def get_exclude(self, request, obj=None):
336                if obj:
337                    return ["sign_date"]
338                return ["name"]
339
340        self.assertEqual(
341            list(BandAdmin(Band, self.site).get_form(request, self.band).base_fields),
342            ["name", "bio"],
343        )
344
345    def test_custom_form_validation(self):
346        # If a form is specified, it should use it allowing custom validation
347        # to work properly. This won't break any of the admin widgets or media.
348        class AdminBandForm(forms.ModelForm):
349            delete = forms.BooleanField()
350
351        class BandAdmin(ModelAdmin):
352            form = AdminBandForm
353
354        ma = BandAdmin(Band, self.site)
355        self.assertEqual(
356            list(ma.get_form(request).base_fields),
357            ["name", "bio", "sign_date", "delete"],
358        )
359        self.assertEqual(
360            type(ma.get_form(request).base_fields["sign_date"].widget), AdminDateWidget
361        )
362
363    def test_form_exclude_kwarg_override(self):
364        """
365        The `exclude` kwarg passed to `ModelAdmin.get_form()` overrides all
366        other declarations (#8999).
367        """
368
369        class AdminBandForm(forms.ModelForm):
370            class Meta:
371                model = Band
372                exclude = ["name"]
373
374        class BandAdmin(ModelAdmin):
375            exclude = ["sign_date"]
376            form = AdminBandForm
377
378            def get_form(self, request, obj=None, **kwargs):
379                kwargs["exclude"] = ["bio"]
380                return super().get_form(request, obj, **kwargs)
381
382        ma = BandAdmin(Band, self.site)
383        self.assertEqual(list(ma.get_form(request).base_fields), ["name", "sign_date"])
384
385    def test_formset_exclude_kwarg_override(self):
386        """
387        The `exclude` kwarg passed to `InlineModelAdmin.get_formset()`
388        overrides all other declarations (#8999).
389        """
390
391        class AdminConcertForm(forms.ModelForm):
392            class Meta:
393                model = Concert
394                exclude = ["day"]
395
396        class ConcertInline(TabularInline):
397            exclude = ["transport"]
398            form = AdminConcertForm
399            fk_name = "main_band"
400            model = Concert
401
402            def get_formset(self, request, obj=None, **kwargs):
403                kwargs["exclude"] = ["opening_band"]
404                return super().get_formset(request, obj, **kwargs)
405
406        class BandAdmin(ModelAdmin):
407            inlines = [ConcertInline]
408
409        ma = BandAdmin(Band, self.site)
410        self.assertEqual(
411            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
412            ["main_band", "day", "transport", "id", "DELETE"],
413        )
414
415    def test_formset_overriding_get_exclude_with_form_fields(self):
416        class AdminConcertForm(forms.ModelForm):
417            class Meta:
418                model = Concert
419                fields = ["main_band", "opening_band", "day", "transport"]
420
421        class ConcertInline(TabularInline):
422            form = AdminConcertForm
423            fk_name = "main_band"
424            model = Concert
425
426            def get_exclude(self, request, obj=None):
427                return ["opening_band"]
428
429        class BandAdmin(ModelAdmin):
430            inlines = [ConcertInline]
431
432        ma = BandAdmin(Band, self.site)
433        self.assertEqual(
434            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
435            ["main_band", "day", "transport", "id", "DELETE"],
436        )
437
438    def test_formset_overriding_get_exclude_with_form_exclude(self):
439        class AdminConcertForm(forms.ModelForm):
440            class Meta:
441                model = Concert
442                exclude = ["day"]
443
444        class ConcertInline(TabularInline):
445            form = AdminConcertForm
446            fk_name = "main_band"
447            model = Concert
448
449            def get_exclude(self, request, obj=None):
450                return ["opening_band"]
451
452        class BandAdmin(ModelAdmin):
453            inlines = [ConcertInline]
454
455        ma = BandAdmin(Band, self.site)
456        self.assertEqual(
457            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
458            ["main_band", "day", "transport", "id", "DELETE"],
459        )
460
461    def test_raw_id_fields_widget_override(self):
462        """
463        The autocomplete_fields, raw_id_fields, and radio_fields widgets may
464        overridden by specifying a widget in get_formset().
465        """
466
467        class ConcertInline(TabularInline):
468            model = Concert
469            fk_name = "main_band"
470            raw_id_fields = ("opening_band",)
471
472            def get_formset(self, request, obj=None, **kwargs):
473                kwargs["widgets"] = {"opening_band": Select}
474                return super().get_formset(request, obj, **kwargs)
475
476        class BandAdmin(ModelAdmin):
477            inlines = [ConcertInline]
478
479        ma = BandAdmin(Band, self.site)
480        band_widget = (
481            list(ma.get_formsets_with_inlines(request))[0][0]()
482            .forms[0]
483            .fields["opening_band"]
484            .widget
485        )
486        # Without the override this would be ForeignKeyRawIdWidget.
487        self.assertIsInstance(band_widget, Select)
488
489    def test_queryset_override(self):
490        # If the queryset of a ModelChoiceField in a custom form is overridden,
491        # RelatedFieldWidgetWrapper doesn't mess that up.
492        band2 = Band.objects.create(
493            name="The Beatles", bio="", sign_date=date(1962, 1, 1)
494        )
495
496        ma = ModelAdmin(Concert, self.site)
497        form = ma.get_form(request)()
498
499        self.assertHTMLEqual(
500            str(form["main_band"]),
501            '<div class="related-widget-wrapper">'
502            '<select name="main_band" id="id_main_band" required>'
503            '<option value="" selected>---------</option>'
504            '<option value="%d">The Beatles</option>'
505            '<option value="%d">The Doors</option>'
506            "</select></div>" % (band2.id, self.band.id),
507        )
508
509        class AdminConcertForm(forms.ModelForm):
510            def __init__(self, *args, **kwargs):
511                super().__init__(*args, **kwargs)
512                self.fields["main_band"].queryset = Band.objects.filter(
513                    name="The Doors"
514                )
515
516        class ConcertAdminWithForm(ModelAdmin):
517            form = AdminConcertForm
518
519        ma = ConcertAdminWithForm(Concert, self.site)
520        form = ma.get_form(request)()
521
522        self.assertHTMLEqual(
523            str(form["main_band"]),
524            '<div class="related-widget-wrapper">'
525            '<select name="main_band" id="id_main_band" required>'
526            '<option value="" selected>---------</option>'
527            '<option value="%d">The Doors</option>'
528            "</select></div>" % self.band.id,
529        )
530
531    def test_regression_for_ticket_15820(self):
532        """
533        `obj` is passed from `InlineModelAdmin.get_fieldsets()` to
534        `InlineModelAdmin.get_formset()`.
535        """
536
537        class CustomConcertForm(forms.ModelForm):
538            class Meta:
539                model = Concert
540                fields = ["day"]
541
542        class ConcertInline(TabularInline):
543            model = Concert
544            fk_name = "main_band"
545
546            def get_formset(self, request, obj=None, **kwargs):
547                if obj:
548                    kwargs["form"] = CustomConcertForm
549                return super().get_formset(request, obj, **kwargs)
550
551        class BandAdmin(ModelAdmin):
552            inlines = [ConcertInline]
553
554        Concert.objects.create(main_band=self.band, opening_band=self.band, day=1)
555        ma = BandAdmin(Band, self.site)
556        inline_instances = ma.get_inline_instances(request)
557        fieldsets = list(inline_instances[0].get_fieldsets(request))
558        self.assertEqual(
559            fieldsets[0][1]["fields"], ["main_band", "opening_band", "day", "transport"]
560        )
561        fieldsets = list(
562            inline_instances[0].get_fieldsets(request, inline_instances[0].model)
563        )
564        self.assertEqual(fieldsets[0][1]["fields"], ["day"])
565
566    # radio_fields behavior ###########################################
567
568    def test_default_foreign_key_widget(self):
569        # First, without any radio_fields specified, the widgets for ForeignKey
570        # and fields with choices specified ought to be a basic Select widget.
571        # ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
572        # they need to be handled properly when type checking. For Select fields, all of
573        # the choices lists have a first entry of dashes.
574        cma = ModelAdmin(Concert, self.site)
575        cmafa = cma.get_form(request)
576
577        self.assertEqual(type(cmafa.base_fields["main_band"].widget.widget), Select)
578        self.assertEqual(
579            list(cmafa.base_fields["main_band"].widget.choices),
580            [("", "---------"), (self.band.id, "The Doors")],
581        )
582
583        self.assertEqual(type(cmafa.base_fields["opening_band"].widget.widget), Select)
584        self.assertEqual(
585            list(cmafa.base_fields["opening_band"].widget.choices),
586            [("", "---------"), (self.band.id, "The Doors")],
587        )
588        self.assertEqual(type(cmafa.base_fields["day"].widget), Select)
589        self.assertEqual(
590            list(cmafa.base_fields["day"].widget.choices),
591            [("", "---------"), (1, "Fri"), (2, "Sat")],
592        )
593        self.assertEqual(type(cmafa.base_fields["transport"].widget), Select)
594        self.assertEqual(
595            list(cmafa.base_fields["transport"].widget.choices),
596            [("", "---------"), (1, "Plane"), (2, "Train"), (3, "Bus")],
597        )
598
599    def test_foreign_key_as_radio_field(self):
600        # Now specify all the fields as radio_fields.  Widgets should now be
601        # RadioSelect, and the choices list should have a first entry of 'None' if
602        # blank=True for the model field.  Finally, the widget should have the
603        # 'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
604        class ConcertAdmin(ModelAdmin):
605            radio_fields = {
606                "main_band": HORIZONTAL,
607                "opening_band": VERTICAL,
608                "day": VERTICAL,
609                "transport": HORIZONTAL,
610            }
611
612        cma = ConcertAdmin(Concert, self.site)
613        cmafa = cma.get_form(request)
614
615        self.assertEqual(
616            type(cmafa.base_fields["main_band"].widget.widget), AdminRadioSelect
617        )
618        self.assertEqual(
619            cmafa.base_fields["main_band"].widget.attrs, {"class": "radiolist inline"}
620        )
621        self.assertEqual(
622            list(cmafa.base_fields["main_band"].widget.choices),
623            [(self.band.id, "The Doors")],
624        )
625
626        self.assertEqual(
627            type(cmafa.base_fields["opening_band"].widget.widget), AdminRadioSelect
628        )
629        self.assertEqual(
630            cmafa.base_fields["opening_band"].widget.attrs, {"class": "radiolist"}
631        )
632        self.assertEqual(
633            list(cmafa.base_fields["opening_band"].widget.choices),
634            [("", "None"), (self.band.id, "The Doors")],
635        )
636        self.assertEqual(type(cmafa.base_fields["day"].widget), AdminRadioSelect)
637        self.assertEqual(cmafa.base_fields["day"].widget.attrs, {"class": "radiolist"})
638        self.assertEqual(
639            list(cmafa.base_fields["day"].widget.choices), [(1, "Fri"), (2, "Sat")]
640        )
641
642        self.assertEqual(type(cmafa.base_fields["transport"].widget), AdminRadioSelect)
643        self.assertEqual(
644            cmafa.base_fields["transport"].widget.attrs, {"class": "radiolist inline"}
645        )
646        self.assertEqual(
647            list(cmafa.base_fields["transport"].widget.choices),
648            [("", "None"), (1, "Plane"), (2, "Train"), (3, "Bus")],
649        )
650
651        class AdminConcertForm(forms.ModelForm):
652            class Meta:
653                model = Concert
654                exclude = ("transport",)
655
656        class ConcertAdmin(ModelAdmin):
657            form = AdminConcertForm
658
659        ma = ConcertAdmin(Concert, self.site)
660        self.assertEqual(
661            list(ma.get_form(request).base_fields), ["main_band", "opening_band", "day"]
662        )
663
664        class AdminConcertForm(forms.ModelForm):
665            extra = forms.CharField()
666
667            class Meta:
668                model = Concert
669                fields = ["extra", "transport"]
670
671        class ConcertAdmin(ModelAdmin):
672            form = AdminConcertForm
673
674        ma = ConcertAdmin(Concert, self.site)
675        self.assertEqual(list(ma.get_form(request).base_fields), ["extra", "transport"])
676
677        class ConcertInline(TabularInline):
678            form = AdminConcertForm
679            model = Concert
680            fk_name = "main_band"
681            can_delete = True
682
683        class BandAdmin(ModelAdmin):
684            inlines = [ConcertInline]
685
686        ma = BandAdmin(Band, self.site)
687        self.assertEqual(
688            list(list(ma.get_formsets_with_inlines(request))[0][0]().forms[0].fields),
689            ["extra", "transport", "id", "DELETE", "main_band"],
690        )
691
692    def test_log_actions(self):
693        ma = ModelAdmin(Band, self.site)
694        mock_request = MockRequest()
695        mock_request.user = User.objects.create(username="bill")
696        content_type = get_content_type_for_model(self.band)
697        tests = (
698            (ma.log_addition, ADDITION, {"added": {}}),
699            (ma.log_change, CHANGE, {"changed": {"fields": ["name", "bio"]}}),
700            (ma.log_deletion, DELETION, str(self.band)),
701        )
702        for method, flag, message in tests:
703            with self.subTest(name=method.__name__):
704                created = method(mock_request, self.band, message)
705                fetched = LogEntry.objects.filter(action_flag=flag).latest("id")
706                self.assertEqual(created, fetched)
707                self.assertEqual(fetched.action_flag, flag)
708                self.assertEqual(fetched.content_type, content_type)
709                self.assertEqual(fetched.object_id, str(self.band.pk))
710                self.assertEqual(fetched.user, mock_request.user)
711                if flag == DELETION:
712                    self.assertEqual(fetched.change_message, "")
713                    self.assertEqual(fetched.object_repr, message)
714                else:
715                    self.assertEqual(fetched.change_message, str(message))
716                    self.assertEqual(fetched.object_repr, str(self.band))
717
718    def test_get_autocomplete_fields(self):
719        class NameAdmin(ModelAdmin):
720            search_fields = ["name"]
721
722        class SongAdmin(ModelAdmin):
723            autocomplete_fields = ["featuring"]
724            fields = ["featuring", "band"]
725
726        class OtherSongAdmin(SongAdmin):
727            def get_autocomplete_fields(self, request):
728                return ["band"]
729
730        self.site.register(Band, NameAdmin)
731        try:
732            # Uses autocomplete_fields if not overridden.
733            model_admin = SongAdmin(Song, self.site)
734            form = model_admin.get_form(request)()
735            self.assertIsInstance(
736                form.fields["featuring"].widget.widget, AutocompleteSelectMultiple
737            )
738            # Uses overridden get_autocomplete_fields
739            model_admin = OtherSongAdmin(Song, self.site)
740            form = model_admin.get_form(request)()
741            self.assertIsInstance(form.fields["band"].widget.widget, AutocompleteSelect)
742        finally:
743            self.site.unregister(Band)
744
745    def test_get_deleted_objects(self):
746        mock_request = MockRequest()
747        mock_request.user = User.objects.create_superuser(
748            username="bob", email="bob@test.com", password="test"
749        )
750        self.site.register(Band, ModelAdmin)
751        ma = self.site._registry[Band]
752        (
753            deletable_objects,
754            model_count,
755            perms_needed,
756            protected,
757        ) = ma.get_deleted_objects([self.band], request)
758        self.assertEqual(deletable_objects, ["Band: The Doors"])
759        self.assertEqual(model_count, {"bands": 1})
760        self.assertEqual(perms_needed, set())
761        self.assertEqual(protected, [])
762
763    def test_get_deleted_objects_with_custom_has_delete_permission(self):
764        """
765        ModelAdmin.get_deleted_objects() uses ModelAdmin.has_delete_permission()
766        for permissions checking.
767        """
768        mock_request = MockRequest()
769        mock_request.user = User.objects.create_superuser(
770            username="bob", email="bob@test.com", password="test"
771        )
772
773        class TestModelAdmin(ModelAdmin):
774            def has_delete_permission(self, request, obj=None):
775                return False
776
777        self.site.register(Band, TestModelAdmin)
778        ma = self.site._registry[Band]
779        (
780            deletable_objects,
781            model_count,
782            perms_needed,
783            protected,
784        ) = ma.get_deleted_objects([self.band], request)
785        self.assertEqual(deletable_objects, ["Band: The Doors"])
786        self.assertEqual(model_count, {"bands": 1})
787        self.assertEqual(perms_needed, {"band"})
788        self.assertEqual(protected, [])
789
790
791class ModelAdminPermissionTests(SimpleTestCase):
792    class MockUser:
793        def has_module_perms(self, app_label):
794            return app_label == "modeladmin"
795
796    class MockViewUser(MockUser):
797        def has_perm(self, perm):
798            return perm == "modeladmin.view_band"
799
800    class MockAddUser(MockUser):
801        def has_perm(self, perm):
802            return perm == "modeladmin.add_band"
803
804    class MockChangeUser(MockUser):
805        def has_perm(self, perm):
806            return perm == "modeladmin.change_band"
807
808    class MockDeleteUser(MockUser):
809        def has_perm(self, perm):
810            return perm == "modeladmin.delete_band"
811
812    def test_has_view_permission(self):
813        """
814        has_view_permission() returns True for users who can view objects and
815        False for users who can't.
816        """
817        ma = ModelAdmin(Band, AdminSite())
818        request = MockRequest()
819        request.user = self.MockViewUser()
820        self.assertIs(ma.has_view_permission(request), True)
821        request.user = self.MockAddUser()
822        self.assertIs(ma.has_view_permission(request), False)
823        request.user = self.MockChangeUser()
824        self.assertIs(ma.has_view_permission(request), True)
825        request.user = self.MockDeleteUser()
826        self.assertIs(ma.has_view_permission(request), False)
827
828    def test_has_add_permission(self):
829        """
830        has_add_permission returns True for users who can add objects and
831        False for users who can't.
832        """
833        ma = ModelAdmin(Band, AdminSite())
834        request = MockRequest()
835        request.user = self.MockViewUser()
836        self.assertFalse(ma.has_add_permission(request))
837        request.user = self.MockAddUser()
838        self.assertTrue(ma.has_add_permission(request))
839        request.user = self.MockChangeUser()
840        self.assertFalse(ma.has_add_permission(request))
841        request.user = self.MockDeleteUser()
842        self.assertFalse(ma.has_add_permission(request))
843
844    def test_inline_has_add_permission_uses_obj(self):
845        class ConcertInline(TabularInline):
846            model = Concert
847
848            def has_add_permission(self, request, obj):
849                return bool(obj)
850
851        class BandAdmin(ModelAdmin):
852            inlines = [ConcertInline]
853
854        ma = BandAdmin(Band, AdminSite())
855        request = MockRequest()
856        request.user = self.MockAddUser()
857        self.assertEqual(ma.get_inline_instances(request), [])
858        band = Band(name="The Doors", bio="", sign_date=date(1965, 1, 1))
859        inline_instances = ma.get_inline_instances(request, band)
860        self.assertEqual(len(inline_instances), 1)
861        self.assertIsInstance(inline_instances[0], ConcertInline)
862
863    def test_has_change_permission(self):
864        """
865        has_change_permission returns True for users who can edit objects and
866        False for users who can't.
867        """
868        ma = ModelAdmin(Band, AdminSite())
869        request = MockRequest()
870        request.user = self.MockViewUser()
871        self.assertIs(ma.has_change_permission(request), False)
872        request.user = self.MockAddUser()
873        self.assertFalse(ma.has_change_permission(request))
874        request.user = self.MockChangeUser()
875        self.assertTrue(ma.has_change_permission(request))
876        request.user = self.MockDeleteUser()
877        self.assertFalse(ma.has_change_permission(request))
878
879    def test_has_delete_permission(self):
880        """
881        has_delete_permission returns True for users who can delete objects and
882        False for users who can't.
883        """
884        ma = ModelAdmin(Band, AdminSite())
885        request = MockRequest()
886        request.user = self.MockViewUser()
887        self.assertIs(ma.has_delete_permission(request), False)
888        request.user = self.MockAddUser()
889        self.assertFalse(ma.has_delete_permission(request))
890        request.user = self.MockChangeUser()
891        self.assertFalse(ma.has_delete_permission(request))
892        request.user = self.MockDeleteUser()
893        self.assertTrue(ma.has_delete_permission(request))
894
895    def test_has_module_permission(self):
896        """
897        as_module_permission returns True for users who have any permission
898        for the module and False for users who don't.
899        """
900        ma = ModelAdmin(Band, AdminSite())
901        request = MockRequest()
902        request.user = self.MockViewUser()
903        self.assertIs(ma.has_module_permission(request), True)
904        request.user = self.MockAddUser()
905        self.assertTrue(ma.has_module_permission(request))
906        request.user = self.MockChangeUser()
907        self.assertTrue(ma.has_module_permission(request))
908        request.user = self.MockDeleteUser()
909        self.assertTrue(ma.has_module_permission(request))
910
911        original_app_label = ma.opts.app_label
912        ma.opts.app_label = "anotherapp"
913        try:
914            request.user = self.MockViewUser()
915            self.assertIs(ma.has_module_permission(request), False)
916            request.user = self.MockAddUser()
917            self.assertFalse(ma.has_module_permission(request))
918            request.user = self.MockChangeUser()
919            self.assertFalse(ma.has_module_permission(request))
920            request.user = self.MockDeleteUser()
921            self.assertFalse(ma.has_module_permission(request))
922        finally:
923            ma.opts.app_label = original_app_label

tests/admin_views/test_autocomplete_view.py

  1import json
  2from contextlib import contextmanager
  3
  4from django.contrib import admin
  5from django.contrib.admin.tests import AdminSeleniumTestCase
  6from django.contrib.admin.views.autocomplete import AutocompleteJsonView
  7from django.contrib.auth.models import Permission, User
  8from django.contrib.contenttypes.models import ContentType
  9from django.http import Http404
 10from django.test import RequestFactory, override_settings
 11from django.urls import reverse, reverse_lazy
 12
 13from .admin import AnswerAdmin, QuestionAdmin
 14from .models import Answer, Author, Authorship, Book, Question
 15from .tests import AdminViewBasicTestCase
 16
 17PAGINATOR_SIZE = AutocompleteJsonView.paginate_by
 18
 19
 20class AuthorAdmin(admin.ModelAdmin):
 21    ordering = ["id"]
 22    search_fields = ["id"]
 23
 24
 25class AuthorshipInline(admin.TabularInline):
 26    model = Authorship
 27    autocomplete_fields = ["author"]
 28
 29
 30class BookAdmin(admin.ModelAdmin):
 31    inlines = [AuthorshipInline]
 32
 33
 34site = admin.AdminSite(name="autocomplete_admin")
 35site.register(Question, QuestionAdmin)
 36site.register(Answer, AnswerAdmin)
 37site.register(Author, AuthorAdmin)
 38site.register(Book, BookAdmin)
 39
 40
 41class AutocompleteJsonViewTests(AdminViewBasicTestCase):
 42    as_view_args = {"model_admin": QuestionAdmin(Question, site)}
 43    factory = RequestFactory()
 44    url = reverse_lazy("autocomplete_admin:admin_views_question_autocomplete")
 45
 46    @classmethod
 47    def setUpTestData(cls):
 48        cls.user = User.objects.create_user(
 49            username="user", password="secret", email="user@example.com", is_staff=True,
 50        )
 51        super().setUpTestData()
 52
 53    def test_success(self):
 54        q = Question.objects.create(question="Is this a question?")
 55        request = self.factory.get(self.url, {"term": "is"})
 56        request.user = self.superuser
 57        response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
 58        self.assertEqual(response.status_code, 200)
 59        data = json.loads(response.content.decode("utf-8"))
 60        self.assertEqual(
 61            data,
 62            {
 63                "results": [{"id": str(q.pk), "text": q.question}],
 64                "pagination": {"more": False},
 65            },
 66        )
 67
 68    def test_must_be_logged_in(self):
 69        response = self.client.get(self.url, {"term": ""})
 70        self.assertEqual(response.status_code, 200)
 71        self.client.logout()
 72        response = self.client.get(self.url, {"term": ""})
 73        self.assertEqual(response.status_code, 302)
 74
 75    def test_has_view_or_change_permission_required(self):
 76        """
 77        Users require the change permission for the related model to the
 78        autocomplete view for it.
 79        """
 80        request = self.factory.get(self.url, {"term": "is"})
 81        self.user.is_staff = True
 82        self.user.save()
 83        request.user = self.user
 84        response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
 85        self.assertEqual(response.status_code, 403)
 86        self.assertJSONEqual(
 87            response.content.decode("utf-8"), {"error": "403 Forbidden"}
 88        )
 89        for permission in ("view", "change"):
 90            with self.subTest(permission=permission):
 91                self.user.user_permissions.clear()
 92                p = Permission.objects.get(
 93                    content_type=ContentType.objects.get_for_model(Question),
 94                    codename="%s_question" % permission,
 95                )
 96                self.user.user_permissions.add(p)
 97                request.user = User.objects.get(pk=self.user.pk)
 98                response = AutocompleteJsonView.as_view(**self.as_view_args)(request)
 99                self.assertEqual(response.status_code, 200)
100
101    def test_search_use_distinct(self):
102        """
103        Searching across model relations use QuerySet.distinct() to avoid
104        duplicates.
105        """
106        q1 = Question.objects.create(question="question 1")
107        q2 = Question.objects.create(question="question 2")
108        q2.related_questions.add(q1)
109        q3 = Question.objects.create(question="question 3")
110        q3.related_questions.add(q1)
111        request = self.factory.get(self.url, {"term": "question"})
112        request.user = self.superuser
113
114        class DistinctQuestionAdmin(QuestionAdmin):
115            search_fields = ["related_questions__question", "question"]
116
117        model_admin = DistinctQuestionAdmin(Question, site)
118        response = AutocompleteJsonView.as_view(model_admin=model_admin)(request)
119        self.assertEqual(response.status_code, 200)
120        data = json.loads(response.content.decode("utf-8"))
121        self.assertEqual(len(data["results"]), 3)
122
123    def test_missing_search_fields(self):
124        class EmptySearchAdmin(QuestionAdmin):
125            search_fields = []
126
127        model_admin = EmptySearchAdmin(Question, site)
128        msg = "EmptySearchAdmin must have search_fields for the autocomplete_view."
129        with self.assertRaisesMessage(Http404, msg):
130            model_admin.autocomplete_view(self.factory.get(self.url))
131
132    def test_get_paginator(self):
133        """Search results are paginated."""
134        Question.objects.bulk_create(
135            Question(question=str(i)) for i in range(PAGINATOR_SIZE + 10)
136        )
137        model_admin = QuestionAdmin(Question, site)
138        model_admin.ordering = ["pk"]
139        # The first page of results.
140        request = self.factory.get(self.url, {"term": ""})
141        request.user = self.superuser
142        response = AutocompleteJsonView.as_view(model_admin=model_admin)(request)
143        self.assertEqual(response.status_code, 200)
144        data = json.loads(response.content.decode("utf-8"))
145        self.assertEqual(
146            data,
147            {
148                "results": [
149                    {"id": str(q.pk), "text": q.question}
150                    for q in Question.objects.all()[:PAGINATOR_SIZE]
151                ],
152                "pagination": {"more": True},
153            },
154        )
155        # The second page of results.
156        request = self.factory.get(self.url, {"term": "", "page": "2"})
157        request.user = self.superuser
158        response = AutocompleteJsonView.as_view(model_admin=model_admin)(request)
159        self.assertEqual(response.status_code, 200)
160        data = json.loads(response.content.decode("utf-8"))
161        self.assertEqual(
162            data,
163            {
164                "results": [
165                    {"id": str(q.pk), "text": q.question}
166                    for q in Question.objects.all()[PAGINATOR_SIZE:]
167                ],
168                "pagination": {"more": False},
169            },
170        )
171
172
173@override_settings(ROOT_URLCONF="admin_views.urls")
174class SeleniumTests(AdminSeleniumTestCase):
175    available_apps = ["admin_views"] + AdminSeleniumTestCase.available_apps
176
177    def setUp(self):
178        self.superuser = User.objects.create_superuser(
179            username="super", password="secret", email="super@example.com",
180        )
181        self.admin_login(
182            username="super",
183            password="secret",
184            login_url=reverse("autocomplete_admin:index"),
185        )
186
187    @contextmanager
188    def select2_ajax_wait(self, timeout=10):
189        from selenium.common.exceptions import NoSuchElementException
190        from selenium.webdriver.support import expected_conditions as ec
191
192        yield
193        with self.disable_implicit_wait():
194            try:
195                loading_element = self.selenium.find_element_by_css_selector(
196                    "li.select2-results__option.loading-results"
197                )
198            except NoSuchElementException:
199                pass
200            else:
201                self.wait_until(ec.staleness_of(loading_element), timeout=timeout)
202
203    def test_select(self):
204        from selenium.webdriver.common.keys import Keys
205        from selenium.webdriver.support.ui import Select
206
207        self.selenium.get(
208            self.live_server_url + reverse("autocomplete_admin:admin_views_answer_add")
209        )
210        elem = self.selenium.find_element_by_css_selector(".select2-selection")
211        elem.click()  # Open the autocomplete dropdown.
212        results = self.selenium.find_element_by_css_selector(".select2-results")
213        self.assertTrue(results.is_displayed())
214        option = self.selenium.find_element_by_css_selector(".select2-results__option")
215        self.assertEqual(option.text, "No results found")
216        elem.click()  # Close the autocomplete dropdown.
217        q1 = Question.objects.create(question="Who am I?")
218        Question.objects.bulk_create(
219            Question(question=str(i)) for i in range(PAGINATOR_SIZE + 10)
220        )
221        elem.click()  # Reopen the dropdown now that some objects exist.
222        result_container = self.selenium.find_element_by_css_selector(
223            ".select2-results"
224        )
225        self.assertTrue(result_container.is_displayed())
226        results = result_container.find_elements_by_css_selector(
227            ".select2-results__option"
228        )
229        # PAGINATOR_SIZE results and "Loading more results".
230        self.assertEqual(len(results), PAGINATOR_SIZE + 1)
231        search = self.selenium.find_element_by_css_selector(".select2-search__field")
232        # Load next page of results by scrolling to the bottom of the list.
233        with self.select2_ajax_wait():
234            for _ in range(len(results)):
235                search.send_keys(Keys.ARROW_DOWN)
236        results = result_container.find_elements_by_css_selector(
237            ".select2-results__option"
238        )
239        # All objects are now loaded.
240        self.assertEqual(len(results), PAGINATOR_SIZE + 11)
241        # Limit the results with the search field.
242        with self.select2_ajax_wait():
243            search.send_keys("Who")
244            # Ajax request is delayed.
245            self.assertTrue(result_container.is_displayed())
246            results = result_container.find_elements_by_css_selector(
247                ".select2-results__option"
248            )
249            self.assertEqual(len(results), PAGINATOR_SIZE + 12)
250        self.assertTrue(result_container.is_displayed())
251        results = result_container.find_elements_by_css_selector(
252            ".select2-results__option"
253        )
254        self.assertEqual(len(results), 1)
255        # Select the result.
256        search.send_keys(Keys.RETURN)
257        select = Select(self.selenium.find_element_by_id("id_question"))
258        self.assertEqual(
259            select.first_selected_option.get_attribute("value"), str(q1.pk)
260        )
261
262    def test_select_multiple(self):
263        from selenium.webdriver.common.keys import Keys
264        from selenium.webdriver.support.ui import Select
265
266        self.selenium.get(
267            self.live_server_url
268            + reverse("autocomplete_admin:admin_views_question_add")
269        )
270        elem = self.selenium.find_element_by_css_selector(".select2-selection")
271        elem.click()  # Open the autocomplete dropdown.
272        results = self.selenium.find_element_by_css_selector(".select2-results")
273        self.assertTrue(results.is_displayed())
274        option = self.selenium.find_element_by_css_selector(".select2-results__option")
275        self.assertEqual(option.text, "No results found")
276        elem.click()  # Close the autocomplete dropdown.
277        Question.objects.create(question="Who am I?")
278        Question.objects.bulk_create(
279            Question(question=str(i)) for i in range(PAGINATOR_SIZE + 10)
280        )
281        elem.click()  # Reopen the dropdown now that some objects exist.
282        result_container = self.selenium.find_element_by_css_selector(
283            ".select2-results"
284        )
285        self.assertTrue(result_container.is_displayed())
286        results = result_container.find_elements_by_css_selector(
287            ".select2-results__option"
288        )
289        self.assertEqual(len(results), PAGINATOR_SIZE + 1)
290        search = self.selenium.find_element_by_css_selector(".select2-search__field")
291        # Load next page of results by scrolling to the bottom of the list.
292        with self.select2_ajax_wait():
293            for _ in range(len(results)):
294                search.send_keys(Keys.ARROW_DOWN)
295        results = result_container.find_elements_by_css_selector(
296            ".select2-results__option"
297        )
298        self.assertEqual(len(results), 31)
299        # Limit the results with the search field.
300        with self.select2_ajax_wait():
301            search.send_keys("Who")
302            # Ajax request is delayed.
303            self.assertTrue(result_container.is_displayed())
304            results = result_container.find_elements_by_css_selector(
305                ".select2-results__option"
306            )
307            self.assertEqual(len(results), 32)
308        self.assertTrue(result_container.is_displayed())
309        results = result_container.find_elements_by_css_selector(
310            ".select2-results__option"
311        )
312        self.assertEqual(len(results), 1)
313        # Select the result.
314        search.send_keys(Keys.RETURN)
315        # Reopen the dropdown and add the first result to the selection.
316        elem.click()
317        search.send_keys(Keys.ARROW_DOWN)
318        search.send_keys(Keys.RETURN)
319        select = Select(self.selenium.find_element_by_id("id_related_questions"))
320        self.assertEqual(len(select.all_selected_options), 2)
321
322    def test_inline_add_another_widgets(self):
323        def assertNoResults(row):
324            elem = row.find_element_by_css_selector(".select2-selection")
325            elem.click()  # Open the autocomplete dropdown.
326            results = self.selenium.find_element_by_css_selector(".select2-results")
327            self.assertTrue(results.is_displayed())
328            option = self.selenium.find_element_by_css_selector(
329                ".select2-results__option"
330            )
331            self.assertEqual(option.text, "No results found")
332
333        # Autocomplete works in rows present when the page loads.
334        self.selenium.get(
335            self.live_server_url + reverse("autocomplete_admin:admin_views_book_add")
336        )
337        rows = self.selenium.find_elements_by_css_selector(".dynamic-authorship_set")
338        self.assertEqual(len(rows), 3)
339        assertNoResults(rows[0])
340        # Autocomplete works in rows added using the "Add another" button.
341        self.selenium.find_element_by_link_text("Add another Authorship").click()
342        rows = self.selenium.find_elements_by_css_selector(".dynamic-authorship_set")
343        self.assertEqual(len(rows), 4)
344        assertNoResults(rows[-1])

tests/auth_tests/test_management.py

   1import builtins
   2import getpass
   3import os
   4import sys
   5from datetime import date
   6from io import StringIO
   7from unittest import mock
   8
   9from django.apps import apps
  10from django.contrib.auth import get_permission_codename, management
  11from django.contrib.auth.management import (
  12    create_permissions,
  13    get_default_username,
  14)
  15from django.contrib.auth.management.commands import (
  16    changepassword,
  17    createsuperuser,
  18)
  19from django.contrib.auth.models import Group, Permission, User
  20from django.contrib.contenttypes.models import ContentType
  21from django.core.management import call_command
  22from django.core.management.base import CommandError
  23from django.db import migrations
  24from django.test import TestCase, override_settings
  25from django.utils.translation import gettext_lazy as _
  26
  27from .models import (
  28    CustomUser,
  29    CustomUserNonUniqueUsername,
  30    CustomUserWithFK,
  31    CustomUserWithM2M,
  32    Email,
  33    Organization,
  34    UserProxy,
  35)
  36
  37MOCK_INPUT_KEY_TO_PROMPTS = {
  38    # @mock_inputs dict key: [expected prompt messages],
  39    "bypass": ["Bypass password validation and create user anyway? [y/N]: "],
  40    "email": ["Email address: "],
  41    "date_of_birth": ["Date of birth: "],
  42    "first_name": ["First name: "],
  43    "username": [
  44        "Username: ",
  45        lambda: "Username (leave blank to use '%s'): " % get_default_username(),
  46    ],
  47}
  48
  49
  50def mock_inputs(inputs):
  51    """
  52    Decorator to temporarily replace input/getpass to allow interactive
  53    createsuperuser.
  54    """
  55
  56    def inner(test_func):
  57        def wrapped(*args):
  58            class mock_getpass:
  59                @staticmethod
  60                def getpass(prompt=b"Password: ", stream=None):
  61                    if callable(inputs["password"]):
  62                        return inputs["password"]()
  63                    return inputs["password"]
  64
  65            def mock_input(prompt):
  66                assert "__proxy__" not in prompt
  67                response = None
  68                for key, val in inputs.items():
  69                    if val == "KeyboardInterrupt":
  70                        raise KeyboardInterrupt
  71                    # get() fallback because sometimes 'key' is the actual
  72                    # prompt rather than a shortcut name.
  73                    prompt_msgs = MOCK_INPUT_KEY_TO_PROMPTS.get(key, key)
  74                    if isinstance(prompt_msgs, list):
  75                        prompt_msgs = [
  76                            msg() if callable(msg) else msg for msg in prompt_msgs
  77                        ]
  78                    if prompt in prompt_msgs:
  79                        if callable(val):
  80                            response = val()
  81                        else:
  82                            response = val
  83                        break
  84                if response is None:
  85                    raise ValueError("Mock input for %r not found." % prompt)
  86                return response
  87
  88            old_getpass = createsuperuser.getpass
  89            old_input = builtins.input
  90            createsuperuser.getpass = mock_getpass
  91            builtins.input = mock_input
  92            try:
  93                test_func(*args)
  94            finally:
  95                createsuperuser.getpass = old_getpass
  96                builtins.input = old_input
  97
  98        return wrapped
  99
 100    return inner
 101
 102
 103class MockTTY:
 104    """
 105    A fake stdin object that pretends to be a TTY to be used in conjunction
 106    with mock_inputs.
 107    """
 108
 109    def isatty(self):
 110        return True
 111
 112
 113class MockInputTests(TestCase):
 114    @mock_inputs({"username": "alice"})
 115    def test_input_not_found(self):
 116        with self.assertRaisesMessage(
 117            ValueError, "Mock input for 'Email address: ' not found."
 118        ):
 119            call_command("createsuperuser", stdin=MockTTY())
 120
 121
 122class GetDefaultUsernameTestCase(TestCase):
 123    def setUp(self):
 124        self.old_get_system_username = management.get_system_username
 125
 126    def tearDown(self):
 127        management.get_system_username = self.old_get_system_username
 128
 129    def test_actual_implementation(self):
 130        self.assertIsInstance(management.get_system_username(), str)
 131
 132    def test_simple(self):
 133        management.get_system_username = lambda: "joe"
 134        self.assertEqual(management.get_default_username(), "joe")
 135
 136    def test_existing(self):
 137        User.objects.create(username="joe")
 138        management.get_system_username = lambda: "joe"
 139        self.assertEqual(management.get_default_username(), "")
 140        self.assertEqual(management.get_default_username(check_db=False), "joe")
 141
 142    def test_i18n(self):
 143        # 'Julia' with accented 'u':
 144        management.get_system_username = lambda: "J\xfalia"
 145        self.assertEqual(management.get_default_username(), "julia")
 146
 147
 148@override_settings(
 149    AUTH_PASSWORD_VALIDATORS=[
 150        {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
 151    ]
 152)
 153class ChangepasswordManagementCommandTestCase(TestCase):
 154    @classmethod
 155    def setUpTestData(cls):
 156        cls.user = User.objects.create_user(username="joe", password="qwerty")
 157
 158    def setUp(self):
 159        self.stdout = StringIO()
 160        self.stderr = StringIO()
 161
 162    def tearDown(self):
 163        self.stdout.close()
 164        self.stderr.close()
 165
 166    @mock.patch.object(getpass, "getpass", return_value="password")
 167    def test_get_pass(self, mock_get_pass):
 168        call_command("changepassword", username="joe", stdout=self.stdout)
 169        self.assertIs(User.objects.get(username="joe").check_password("password"), True)
 170
 171    @mock.patch.object(getpass, "getpass", return_value="")
 172    def test_get_pass_no_input(self, mock_get_pass):
 173        with self.assertRaisesMessage(CommandError, "aborted"):
 174            call_command("changepassword", username="joe", stdout=self.stdout)
 175
 176    @mock.patch.object(changepassword.Command, "_get_pass", return_value="new_password")
 177    def test_system_username(self, mock_get_pass):
 178        """The system username is used if --username isn't provided."""
 179        username = getpass.getuser()
 180        User.objects.create_user(username=username, password="qwerty")
 181        call_command("changepassword", stdout=self.stdout)
 182        self.assertIs(
 183            User.objects.get(username=username).check_password("new_password"), True
 184        )
 185
 186    def test_nonexistent_username(self):
 187        with self.assertRaisesMessage(CommandError, "user 'test' does not exist"):
 188            call_command("changepassword", username="test", stdout=self.stdout)
 189
 190    @mock.patch.object(changepassword.Command, "_get_pass", return_value="not qwerty")
 191    def test_that_changepassword_command_changes_joes_password(self, mock_get_pass):
 192        "Executing the changepassword management command should change joe's password"
 193        self.assertTrue(self.user.check_password("qwerty"))
 194
 195        call_command("changepassword", username="joe", stdout=self.stdout)
 196        command_output = self.stdout.getvalue().strip()
 197
 198        self.assertEqual(
 199            command_output,
 200            "Changing password for user 'joe'\nPassword changed successfully for user 'joe'",
 201        )
 202        self.assertTrue(User.objects.get(username="joe").check_password("not qwerty"))
 203
 204    @mock.patch.object(
 205        changepassword.Command, "_get_pass", side_effect=lambda *args: str(args)
 206    )
 207    def test_that_max_tries_exits_1(self, mock_get_pass):
 208        """
 209        A CommandError should be thrown by handle() if the user enters in
 210        mismatched passwords three times.
 211        """
 212        msg = "Aborting password change for user 'joe' after 3 attempts"
 213        with self.assertRaisesMessage(CommandError, msg):
 214            call_command(
 215                "changepassword", username="joe", stdout=self.stdout, stderr=self.stderr
 216            )
 217
 218    @mock.patch.object(changepassword.Command, "_get_pass", return_value="1234567890")
 219    def test_password_validation(self, mock_get_pass):
 220        """
 221        A CommandError should be raised if the user enters in passwords which
 222        fail validation three times.
 223        """
 224        abort_msg = "Aborting password change for user 'joe' after 3 attempts"
 225        with self.assertRaisesMessage(CommandError, abort_msg):
 226            call_command(
 227                "changepassword", username="joe", stdout=self.stdout, stderr=self.stderr
 228            )
 229        self.assertIn("This password is entirely numeric.", self.stderr.getvalue())
 230
 231    @mock.patch.object(changepassword.Command, "_get_pass", return_value="not qwerty")
 232    def test_that_changepassword_command_works_with_nonascii_output(
 233        self, mock_get_pass
 234    ):
 235        """
 236        #21627 -- Executing the changepassword management command should allow
 237        non-ASCII characters from the User object representation.
 238        """
 239        # 'Julia' with accented 'u':
 240        User.objects.create_user(username="J\xfalia", password="qwerty")
 241        call_command("changepassword", username="J\xfalia", stdout=self.stdout)
 242
 243
 244class MultiDBChangepasswordManagementCommandTestCase(TestCase):
 245    databases = {"default", "other"}
 246
 247    @mock.patch.object(changepassword.Command, "_get_pass", return_value="not qwerty")
 248    def test_that_changepassword_command_with_database_option_uses_given_db(
 249        self, mock_get_pass
 250    ):
 251        """
 252        changepassword --database should operate on the specified DB.
 253        """
 254        user = User.objects.db_manager("other").create_user(
 255            username="joe", password="qwerty"
 256        )
 257        self.assertTrue(user.check_password("qwerty"))
 258
 259        out = StringIO()
 260        call_command("changepassword", username="joe", database="other", stdout=out)
 261        command_output = out.getvalue().strip()
 262
 263        self.assertEqual(
 264            command_output,
 265            "Changing password for user 'joe'\nPassword changed successfully for user 'joe'",
 266        )
 267        self.assertTrue(
 268            User.objects.using("other").get(username="joe").check_password("not qwerty")
 269        )
 270
 271
 272@override_settings(
 273    SILENCED_SYSTEM_CHECKS=["fields.W342"],  # ForeignKey(unique=True)
 274    AUTH_PASSWORD_VALIDATORS=[
 275        {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}
 276    ],
 277)
 278class CreatesuperuserManagementCommandTestCase(TestCase):
 279    def test_no_email_argument(self):
 280        new_io = StringIO()
 281        with self.assertRaisesMessage(
 282            CommandError, "You must use --email with --noinput."
 283        ):
 284            call_command(
 285                "createsuperuser", interactive=False, username="joe", stdout=new_io
 286            )
 287
 288    def test_basic_usage(self):
 289        "Check the operation of the createsuperuser management command"
 290        # We can use the management command to create a superuser
 291        new_io = StringIO()
 292        call_command(
 293            "createsuperuser",
 294            interactive=False,
 295            username="joe",
 296            email="joe@somewhere.org",
 297            stdout=new_io,
 298        )
 299        command_output = new_io.getvalue().strip()
 300        self.assertEqual(command_output, "Superuser created successfully.")
 301        u = User.objects.get(username="joe")
 302        self.assertEqual(u.email, "joe@somewhere.org")
 303
 304        # created password should be unusable
 305        self.assertFalse(u.has_usable_password())
 306
 307    def test_non_ascii_verbose_name(self):
 308        @mock_inputs(
 309            {
 310                "password": "nopasswd",
 311                "Uživatel (leave blank to use '%s'): "
 312                % get_default_username(): "foo",  # username (cz)
 313                "email": "nolocale@somewhere.org",
 314            }
 315        )
 316        def test(self):
 317            username_field = User._meta.get_field("username")
 318            old_verbose_name = username_field.verbose_name
 319            username_field.verbose_name = _("u\u017eivatel")
 320            new_io = StringIO()
 321            try:
 322                call_command(
 323                    "createsuperuser", interactive=True, stdout=new_io, stdin=MockTTY(),
 324                )
 325            finally:
 326                username_field.verbose_name = old_verbose_name
 327
 328            command_output = new_io.getvalue().strip()
 329            self.assertEqual(command_output, "Superuser created successfully.")
 330
 331        test(self)
 332
 333    def test_verbosity_zero(self):
 334        # We can suppress output on the management command
 335        new_io = StringIO()
 336        call_command(
 337            "createsuperuser",
 338            interactive=False,
 339            username="joe2",
 340            email="joe2@somewhere.org",
 341            verbosity=0,
 342            stdout=new_io,
 343        )
 344        command_output = new_io.getvalue().strip()
 345        self.assertEqual(command_output, "")
 346        u = User.objects.get(username="joe2")
 347        self.assertEqual(u.email, "joe2@somewhere.org")
 348        self.assertFalse(u.has_usable_password())
 349
 350    def test_email_in_username(self):
 351        new_io = StringIO()
 352        call_command(
 353            "createsuperuser",
 354            interactive=False,
 355            username="joe+admin@somewhere.org",
 356            email="joe@somewhere.org",
 357            stdout=new_io,
 358        )
 359        u = User._default_manager.get(username="joe+admin@somewhere.org")
 360        self.assertEqual(u.email, "joe@somewhere.org")
 361        self.assertFalse(u.has_usable_password())
 362
 363    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
 364    def test_swappable_user(self):
 365        "A superuser can be created when a custom user model is in use"
 366        # We can use the management command to create a superuser
 367        # We skip validation because the temporary substitution of the
 368        # swappable User model messes with validation.
 369        new_io = StringIO()
 370        call_command(
 371            "createsuperuser",
 372            interactive=False,
 373            email="joe@somewhere.org",
 374            date_of_birth="1976-04-01",
 375            first_name="Joe",
 376            stdout=new_io,
 377        )
 378        command_output = new_io.getvalue().strip()
 379        self.assertEqual(command_output, "Superuser created successfully.")
 380        u = CustomUser._default_manager.get(email="joe@somewhere.org")
 381        self.assertEqual(u.date_of_birth, date(1976, 4, 1))
 382
 383        # created password should be unusable
 384        self.assertFalse(u.has_usable_password())
 385
 386    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUser")
 387    def test_swappable_user_missing_required_field(self):
 388        "A Custom superuser won't be created when a required field isn't provided"
 389        # We can use the management command to create a superuser
 390        # We skip validation because the temporary substitution of the
 391        # swappable User model messes with validation.
 392        new_io = StringIO()
 393        with self.assertRaisesMessage(
 394            CommandError, "You must use --email with --noinput."
 395        ):
 396            call_command(
 397                "createsuperuser", interactive=False, stdout=new_io, stderr=new_io,
 398            )
 399
 400        self.assertEqual(CustomUser._default_manager.count(), 0)
 401
 402    @override_settings(
 403        AUTH_USER_MODEL="auth_tests.CustomUserNonUniqueUsername",
 404        AUTHENTICATION_BACKENDS=["my.custom.backend"],
 405    )
 406    def test_swappable_user_username_non_unique(self):
 407        @mock_inputs(
 408            {"username": "joe", "password": "nopasswd",}
 409        )
 410        def createsuperuser():
 411            new_io = StringIO()
 412            call_command(
 413                "createsuperuser",
 414                interactive=True,
 415                email="joe@somewhere.org",
 416                stdout=new_io,
 417                stdin=MockTTY(),
 418            )
 419            command_output = new_io.getvalue().strip()
 420            self.assertEqual(command_output, "Superuser created successfully.")
 421
 422        for i in range(2):
 423            createsuperuser()
 424
 425        users = CustomUserNonUniqueUsername.objects.filter(username="joe")
 426        self.assertEqual(users.count(), 2)
 427
 428    def test_skip_if_not_in_TTY(self):
 429        """
 430        If the command is not called from a TTY, it should be skipped and a
 431        message should be displayed (#7423).
 432        """
 433
 434        class FakeStdin:
 435            """A fake stdin object that has isatty() return False."""
 436
 437            def isatty(self):
 438                return False
 439
 440        out = StringIO()
 441        call_command(
 442            "createsuperuser", stdin=FakeStdin(), stdout=out, interactive=True,
 443        )
 444
 445        self.assertEqual(User._default_manager.count(), 0)
 446        self.assertIn("Superuser creation skipped", out.getvalue())
 447
 448    def test_passing_stdin(self):
 449        """
 450        You can pass a stdin object as an option and it should be
 451        available on self.stdin.
 452        If no such option is passed, it defaults to sys.stdin.
 453        """
 454        sentinel = object()
 455        command = createsuperuser.Command()
 456        call_command(
 457            command,
 458            stdin=sentinel,
 459            stdout=StringIO(),
 460            stderr=StringIO(),
 461            interactive=False,
 462            verbosity=0,
 463            username="janet",
 464            email="janet@example.com",
 465        )
 466        self.assertIs(command.stdin, sentinel)
 467
 468        command = createsuperuser.Command()
 469        call_command(
 470            command,
 471            stdout=StringIO(),
 472            stderr=StringIO(),
 473            interactive=False,
 474            verbosity=0,
 475            username="joe",
 476            email="joe@example.com",
 477        )
 478        self.assertIs(command.stdin, sys.stdin)
 479
 480    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithFK")
 481    def test_fields_with_fk(self):
 482        new_io = StringIO()
 483        group = Group.objects.create(name="mygroup")
 484        email = Email.objects.create(email="mymail@gmail.com")
 485        call_command(
 486            "createsuperuser",
 487            interactive=False,
 488            username=email.pk,
 489            email=email.email,
 490            group=group.pk,
 491            stdout=new_io,
 492        )
 493        command_output = new_io.getvalue().strip()
 494        self.assertEqual(command_output, "Superuser created successfully.")
 495        u = CustomUserWithFK._default_manager.get(email=email)
 496        self.assertEqual(u.username, email)
 497        self.assertEqual(u.group, group)
 498
 499        non_existent_email = "mymail2@gmail.com"
 500        msg = "email instance with email %r does not exist." % non_existent_email
 501        with self.assertRaisesMessage(CommandError, msg):
 502            call_command(
 503                "createsuperuser",
 504                interactive=False,
 505                username=email.pk,
 506                email=non_existent_email,
 507                stdout=new_io,
 508            )
 509
 510    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithFK")
 511    def test_fields_with_fk_interactive(self):
 512        new_io = StringIO()
 513        group = Group.objects.create(name="mygroup")
 514        email = Email.objects.create(email="mymail@gmail.com")
 515
 516        @mock_inputs(
 517            {
 518                "password": "nopasswd",
 519                "Username (Email.id): ": email.pk,
 520                "Email (Email.email): ": email.email,
 521                "Group (Group.id): ": group.pk,
 522            }
 523        )
 524        def test(self):
 525            call_command(
 526                "createsuperuser", interactive=True, stdout=new_io, stdin=MockTTY(),
 527            )
 528
 529            command_output = new_io.getvalue().strip()
 530            self.assertEqual(command_output, "Superuser created successfully.")
 531            u = CustomUserWithFK._default_manager.get(email=email)
 532            self.assertEqual(u.username, email)
 533            self.assertEqual(u.group, group)
 534
 535        test(self)
 536
 537    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithM2m")
 538    def test_fields_with_m2m(self):
 539        new_io = StringIO()
 540        org_id_1 = Organization.objects.create(name="Organization 1").pk
 541        org_id_2 = Organization.objects.create(name="Organization 2").pk
 542        call_command(
 543            "createsuperuser",
 544            interactive=False,
 545            username="joe",
 546            orgs=[org_id_1, org_id_2],
 547            stdout=new_io,
 548        )
 549        command_output = new_io.getvalue().strip()
 550        self.assertEqual(command_output, "Superuser created successfully.")
 551        user = CustomUserWithM2M._default_manager.get(username="joe")
 552        self.assertEqual(user.orgs.count(), 2)
 553
 554    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithM2M")
 555    def test_fields_with_m2m_interactive(self):
 556        new_io = StringIO()
 557        org_id_1 = Organization.objects.create(name="Organization 1").pk
 558        org_id_2 = Organization.objects.create(name="Organization 2").pk
 559
 560        @mock_inputs(
 561            {
 562                "password": "nopasswd",
 563                "Username: ": "joe",
 564                "Orgs (Organization.id): ": "%s, %s" % (org_id_1, org_id_2),
 565            }
 566        )
 567        def test(self):
 568            call_command(
 569                "createsuperuser", interactive=True, stdout=new_io, stdin=MockTTY(),
 570            )
 571            command_output = new_io.getvalue().strip()
 572            self.assertEqual(command_output, "Superuser created successfully.")
 573            user = CustomUserWithM2M._default_manager.get(username="joe")
 574            self.assertEqual(user.orgs.count(), 2)
 575
 576        test(self)
 577
 578    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithM2M")
 579    def test_fields_with_m2m_interactive_blank(self):
 580        new_io = StringIO()
 581        org_id = Organization.objects.create(name="Organization").pk
 582        entered_orgs = [str(org_id), " "]
 583
 584        def return_orgs():
 585            return entered_orgs.pop()
 586
 587        @mock_inputs(
 588            {
 589                "password": "nopasswd",
 590                "Username: ": "joe",
 591                "Orgs (Organization.id): ": return_orgs,
 592            }
 593        )
 594        def test(self):
 595            call_command(
 596                "createsuperuser",
 597                interactive=True,
 598                stdout=new_io,
 599                stderr=new_io,
 600                stdin=MockTTY(),
 601            )
 602            self.assertEqual(
 603                new_io.getvalue().strip(),
 604                "Error: This field cannot be blank.\n"
 605                "Superuser created successfully.",
 606            )
 607
 608        test(self)
 609
 610    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserWithM2MThrough")
 611    def test_fields_with_m2m_and_through(self):
 612        msg = (
 613            "Required field 'orgs' specifies a many-to-many relation through "
 614            "model, which is not supported."
 615        )
 616        with self.assertRaisesMessage(CommandError, msg):
 617            call_command("createsuperuser")
 618
 619    def test_default_username(self):
 620        """createsuperuser uses a default username when one isn't provided."""
 621        # Get the default username before creating a user.
 622        default_username = get_default_username()
 623        new_io = StringIO()
 624        entered_passwords = ["password", "password"]
 625
 626        def return_passwords():
 627            return entered_passwords.pop(0)
 628
 629        @mock_inputs({"password": return_passwords, "username": "", "email": ""})
 630        def test(self):
 631            call_command(
 632                "createsuperuser",
 633                interactive=True,
 634                stdin=MockTTY(),
 635                stdout=new_io,
 636                stderr=new_io,
 637            )
 638            self.assertEqual(
 639                new_io.getvalue().strip(), "Superuser created successfully."
 640            )
 641            self.assertTrue(User.objects.filter(username=default_username).exists())
 642
 643        test(self)
 644
 645    def test_password_validation(self):
 646        """
 647        Creation should fail if the password fails validation.
 648        """
 649        new_io = StringIO()
 650        entered_passwords = ["1234567890", "1234567890", "password", "password"]
 651
 652        def bad_then_good_password():
 653            return entered_passwords.pop(0)
 654
 655        @mock_inputs(
 656            {
 657                "password": bad_then_good_password,
 658                "username": "joe1234567890",
 659                "email": "",
 660                "bypass": "n",
 661            }
 662        )
 663        def test(self):
 664            call_command(
 665                "createsuperuser",
 666                interactive=True,
 667                stdin=MockTTY(),
 668                stdout=new_io,
 669                stderr=new_io,
 670            )
 671            self.assertEqual(
 672                new_io.getvalue().strip(),
 673                "This password is entirely numeric.\n"
 674                "Superuser created successfully.",
 675            )
 676
 677        test(self)
 678
 679    @override_settings(
 680        AUTH_PASSWORD_VALIDATORS=[
 681            {
 682                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 683            },
 684        ]
 685    )
 686    def test_validate_password_against_username(self):
 687        new_io = StringIO()
 688        username = "supremelycomplex"
 689        entered_passwords = [
 690            username,
 691            username,
 692            "superduperunguessablepassword",
 693            "superduperunguessablepassword",
 694        ]
 695
 696        def bad_then_good_password():
 697            return entered_passwords.pop(0)
 698
 699        @mock_inputs(
 700            {
 701                "password": bad_then_good_password,
 702                "username": username,
 703                "email": "",
 704                "bypass": "n",
 705            }
 706        )
 707        def test(self):
 708            call_command(
 709                "createsuperuser",
 710                interactive=True,
 711                stdin=MockTTY(),
 712                stdout=new_io,
 713                stderr=new_io,
 714            )
 715            self.assertEqual(
 716                new_io.getvalue().strip(),
 717                "The password is too similar to the username.\n"
 718                "Superuser created successfully.",
 719            )
 720
 721        test(self)
 722
 723    @override_settings(
 724        AUTH_USER_MODEL="auth_tests.CustomUser",
 725        AUTH_PASSWORD_VALIDATORS=[
 726            {
 727                "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
 728            },
 729        ],
 730    )
 731    def test_validate_password_against_required_fields(self):
 732        new_io = StringIO()
 733        first_name = "josephine"
 734        entered_passwords = [
 735            first_name,
 736            first_name,
 737            "superduperunguessablepassword",
 738            "superduperunguessablepassword",
 739        ]
 740
 741        def bad_then_good_password():
 742            return entered_passwords.pop(0)
 743
 744        @mock_inputs(
 745            {
 746                "password": bad_then_good_password,
 747                "username": "whatever",
 748                "first_name": first_name,
 749                "date_of_birth": "1970-01-01",
 750                "email": "joey@example.com",
 751                "bypass": "n",
 752            }
 753        )
 754        def test(self):
 755            call_command(
 756                "createsuperuser",
 757                interactive=True,
 758                stdin=MockTTY(),
 759                stdout=new_io,
 760                stderr=new_io,
 761            )
 762            self.assertEqual(
 763                new_io.getvalue().strip(),
 764                "The password is too similar to the first name.\n"
 765                "Superuser created successfully.",
 766            )
 767
 768        test(self)
 769
 770    def test_blank_username(self):
 771        """Creation fails if --username is blank."""
 772        new_io = StringIO()
 773
 774        def test(self):
 775            with self.assertRaisesMessage(CommandError, "Username cannot be blank."):
 776                call_command(
 777                    "createsuperuser",
 778                    username="",
 779                    stdin=MockTTY(),
 780                    stdout=new_io,
 781                    stderr=new_io,
 782                )
 783
 784        test(self)
 785
 786    def test_blank_username_non_interactive(self):
 787        new_io = StringIO()
 788
 789        def test(self):
 790            with self.assertRaisesMessage(CommandError, "Username cannot be blank."):
 791                call_command(
 792                    "createsuperuser",
 793                    username="",
 794                    interactive=False,
 795                    stdin=MockTTY(),
 796                    stdout=new_io,
 797                    stderr=new_io,
 798                )
 799
 800        test(self)
 801
 802    def test_password_validation_bypass(self):
 803        """
 804        Password validation can be bypassed by entering 'y' at the prompt.
 805        """
 806        new_io = StringIO()
 807
 808        @mock_inputs(
 809            {
 810                "password": "1234567890",
 811                "username": "joe1234567890",
 812                "email": "",
 813                "bypass": "y",
 814            }
 815        )
 816        def test(self):
 817            call_command(
 818                "createsuperuser",
 819                interactive=True,
 820                stdin=MockTTY(),
 821                stdout=new_io,
 822                stderr=new_io,
 823            )
 824            self.assertEqual(
 825                new_io.getvalue().strip(),
 826                "This password is entirely numeric.\n"
 827                "Superuser created successfully.",
 828            )
 829
 830        test(self)
 831
 832    def test_invalid_username(self):
 833        """Creation fails if the username fails validation."""
 834        user_field = User._meta.get_field(User.USERNAME_FIELD)
 835        new_io = StringIO()
 836        entered_passwords = ["password", "password"]
 837        # Enter an invalid (too long) username first and then a valid one.
 838        invalid_username = ("x" * user_field.max_length) + "y"
 839        entered_usernames = [invalid_username, "janet"]
 840
 841        def return_passwords():
 842            return entered_passwords.pop(0)
 843
 844        def return_usernames():
 845            return entered_usernames.pop(0)
 846
 847        @mock_inputs(
 848            {"password": return_passwords, "username": return_usernames, "email": ""}
 849        )
 850        def test(self):
 851            call_command(
 852                "createsuperuser",
 853                interactive=True,
 854                stdin=MockTTY(),
 855                stdout=new_io,
 856                stderr=new_io,
 857            )
 858            self.assertEqual(
 859                new_io.getvalue().strip(),
 860                "Error: Ensure this value has at most %s characters (it has %s).\n"
 861                "Superuser created successfully."
 862                % (user_field.max_length, len(invalid_username)),
 863            )
 864
 865        test(self)
 866
 867    @mock_inputs({"username": "KeyboardInterrupt"})
 868    def test_keyboard_interrupt(self):
 869        new_io = StringIO()
 870        with self.assertRaises(SystemExit):
 871            call_command(
 872                "createsuperuser",
 873                interactive=True,
 874                stdin=MockTTY(),
 875                stdout=new_io,
 876                stderr=new_io,
 877            )
 878        self.assertEqual(new_io.getvalue(), "\nOperation cancelled.\n")
 879
 880    def test_existing_username(self):
 881        """Creation fails if the username already exists."""
 882        user = User.objects.create(username="janet")
 883        new_io = StringIO()
 884        entered_passwords = ["password", "password"]
 885        # Enter the existing username first and then a new one.
 886        entered_usernames = [user.username, "joe"]
 887
 888        def return_passwords():
 889            return entered_passwords.pop(0)
 890
 891        def return_usernames():
 892            return entered_usernames.pop(0)
 893
 894        @mock_inputs(
 895            {"password": return_passwords, "username": return_usernames, "email": ""}
 896        )
 897        def test(self):
 898            call_command(
 899                "createsuperuser",
 900                interactive=True,
 901                stdin=MockTTY(),
 902                stdout=new_io,
 903                stderr=new_io,
 904            )
 905            self.assertEqual(
 906                new_io.getvalue().strip(),
 907                "Error: That username is already taken.\n"
 908                "Superuser created successfully.",
 909            )
 910
 911        test(self)
 912
 913    def test_existing_username_non_interactive(self):
 914        """Creation fails if the username already exists."""
 915        User.objects.create(username="janet")
 916        new_io = StringIO()
 917        with self.assertRaisesMessage(
 918            CommandError, "Error: That username is already taken."
 919        ):
 920            call_command(
 921                "createsuperuser",
 922                username="janet",
 923                email="",
 924                interactive=False,
 925                stdout=new_io,
 926            )
 927
 928    def test_existing_username_provided_via_option_and_interactive(self):
 929        """call_command() gets username='janet' and interactive=True."""
 930        new_io = StringIO()
 931        entered_passwords = ["password", "password"]
 932        User.objects.create(username="janet")
 933
 934        def return_passwords():
 935            return entered_passwords.pop(0)
 936
 937        @mock_inputs(
 938            {
 939                "password": return_passwords,
 940                "username": "janet1",
 941                "email": "test@test.com",
 942            }
 943        )
 944        def test(self):
 945            call_command(
 946                "createsuperuser",
 947                username="janet",
 948                interactive=True,
 949                stdin=MockTTY(),
 950                stdout=new_io,
 951                stderr=new_io,
 952            )
 953            msg = "Error: That username is already taken.\nSuperuser created successfully."
 954            self.assertEqual(new_io.getvalue().strip(), msg)
 955
 956        test(self)
 957
 958    def test_validation_mismatched_passwords(self):
 959        """
 960        Creation should fail if the user enters mismatched passwords.
 961        """
 962        new_io = StringIO()
 963
 964        # The first two passwords do not match, but the second two do match and
 965        # are valid.
 966        entered_passwords = ["password", "not password", "password2", "password2"]
 967
 968        def mismatched_passwords_then_matched():
 969            return entered_passwords.pop(0)
 970
 971        @mock_inputs(
 972            {
 973                "password": mismatched_passwords_then_matched,
 974                "username": "joe1234567890",
 975                "email": "",
 976            }
 977        )
 978        def test(self):
 979            call_command(
 980                "createsuperuser",
 981                interactive=True,
 982                stdin=MockTTY(),
 983                stdout=new_io,
 984                stderr=new_io,
 985            )
 986            self.assertEqual(
 987                new_io.getvalue().strip(),
 988                "Error: Your passwords didn't match.\n"
 989                "Superuser created successfully.",
 990            )
 991
 992        test(self)
 993
 994    def test_validation_blank_password_entered(self):
 995        """
 996        Creation should fail if the user enters blank passwords.
 997        """
 998        new_io = StringIO()
 999
1000        # The first two passwords are empty strings, but the second two are
1001        # valid.
1002        entered_passwords = ["", "", "password2", "password2"]
1003
1004        def blank_passwords_then_valid():
1005            return entered_passwords.pop(0)
1006
1007        @mock_inputs(
1008            {
1009                "password": blank_passwords_then_valid,
1010                "username": "joe1234567890",
1011                "email": "",
1012            }
1013        )
1014        def test(self):
1015            call_command(
1016                "createsuperuser",
1017                interactive=True,
1018                stdin=MockTTY(),
1019                stdout=new_io,
1020                stderr=new_io,
1021            )
1022            self.assertEqual(
1023                new_io.getvalue().strip(),
1024                "Error: Blank passwords aren't allowed.\n"
1025                "Superuser created successfully.",
1026            )
1027
1028        test(self)
1029
1030    @override_settings(AUTH_USER_MODEL="auth_tests.NoPasswordUser")
1031    def test_usermodel_without_password(self):
1032        new_io = StringIO()
1033
1034        def test(self):
1035            call_command(
1036                "createsuperuser",
1037                interactive=False,
1038                stdin=MockTTY(),
1039                stdout=new_io,
1040                stderr=new_io,
1041                username="username",
1042            )
1043            self.assertEqual(
1044                new_io.getvalue().strip(), "Superuser created successfully."
1045            )
1046
1047        test(self)
1048
1049    @override_settings(AUTH_USER_MODEL="auth_tests.NoPasswordUser")
1050    def test_usermodel_without_password_interactive(self):
1051        new_io = StringIO()
1052
1053        @mock_inputs({"username": "username"})
1054        def test(self):
1055            call_command(
1056                "createsuperuser",
1057                interactive=True,
1058                stdin=MockTTY(),
1059                stdout=new_io,
1060                stderr=new_io,
1061            )
1062            self.assertEqual(
1063                new_io.getvalue().strip(), "Superuser created successfully."
1064            )
1065
1066        test(self)
1067
1068    @mock.patch.dict(
1069        os.environ,
1070        {
1071            "DJANGO_SUPERUSER_PASSWORD": "test_password",
1072            "DJANGO_SUPERUSER_USERNAME": "test_superuser",
1073            "DJANGO_SUPERUSER_EMAIL": "joe@somewhere.org",
1074            "DJANGO_SUPERUSER_FIRST_NAME": "ignored_first_name",
1075        },
1076    )
1077    def test_environment_variable_non_interactive(self):
1078        call_command("createsuperuser", interactive=False, stdout=StringIO())
1079        user = User.objects.get(username="test_superuser")
1080        self.assertEqual(user.email, "joe@somewhere.org")
1081        self.assertTrue(user.check_password("test_password"))
1082        # Environment variables are ignored for non-required fields.
1083        self.assertEqual(user.first_name, "")
1084
1085    @mock.patch.dict(
1086        os.environ,
1087        {
1088            "DJANGO_SUPERUSER_USERNAME": "test_superuser",
1089            "DJANGO_SUPERUSER_EMAIL": "joe@somewhere.org",
1090        },
1091    )
1092    def test_ignore_environment_variable_non_interactive(self):
1093        # Environment variables are ignored in non-interactive mode, if
1094        # provided by a command line arguments.
1095        call_command(
1096            "createsuperuser",
1097            interactive=False,
1098            username="cmd_superuser",
1099            email="cmd@somewhere.org",
1100            stdout=StringIO(),
1101        )
1102        user = User.objects.get(username="cmd_superuser")
1103        self.assertEqual(user.email, "cmd@somewhere.org")
1104        self.assertFalse(user.has_usable_password())
1105
1106    @mock.patch.dict(
1107        os.environ,
1108        {
1109            "DJANGO_SUPERUSER_PASSWORD": "test_password",
1110            "DJANGO_SUPERUSER_USERNAME": "test_superuser",
1111            "DJANGO_SUPERUSER_EMAIL": "joe@somewhere.org",
1112        },
1113    )
1114    def test_ignore_environment_variable_interactive(self):
1115        # Environment variables are ignored in interactive mode.
1116        @mock_inputs({"password": "cmd_password"})
1117        def test(self):
1118            call_command(
1119                "createsuperuser",
1120                interactive=True,
1121                username="cmd_superuser",
1122                email="cmd@somewhere.org",
1123                stdin=MockTTY(),
1124                stdout=StringIO(),
1125            )
1126            user = User.objects.get(username="cmd_superuser")
1127            self.assertEqual(user.email, "cmd@somewhere.org")
1128            self.assertTrue(user.check_password("cmd_password"))
1129
1130        test(self)
1131
1132
1133class MultiDBCreatesuperuserTestCase(TestCase):
1134    databases = {"default", "other"}
1135
1136    def test_createsuperuser_command_with_database_option(self):
1137        """
1138        changepassword --database should operate on the specified DB.
1139        """
1140        new_io = StringIO()
1141        call_command(
1142            "createsuperuser",
1143            interactive=False,
1144            username="joe",
1145            email="joe@somewhere.org",
1146            database="other",
1147            stdout=new_io,
1148        )
1149        command_output = new_io.getvalue().strip()
1150        self.assertEqual(command_output, "Superuser created successfully.")
1151        user = User.objects.using("other").get(username="joe")
1152        self.assertEqual(user.email, "joe@somewhere.org")
1153
1154
1155class CreatePermissionsTests(TestCase):
1156    def setUp(self):
1157        self._original_permissions = Permission._meta.permissions[:]
1158        self._original_default_permissions = Permission._meta.default_permissions
1159        self.app_config = apps.get_app_config("auth")
1160
1161    def tearDown(self):
1162        Permission._meta.permissions = self._original_permissions
1163        Permission._meta.default_permissions = self._original_default_permissions
1164        ContentType.objects.clear_cache()
1165
1166    def test_default_permissions(self):
1167        permission_content_type = ContentType.objects.get_by_natural_key(
1168            "auth", "permission"
1169        )
1170        Permission._meta.permissions = [
1171            ("my_custom_permission", "Some permission"),
1172        ]
1173        create_permissions(self.app_config, verbosity=0)
1174
1175        # view/add/change/delete permission by default + custom permission
1176        self.assertEqual(
1177            Permission.objects.filter(content_type=permission_content_type,).count(), 5
1178        )
1179
1180        Permission.objects.filter(content_type=permission_content_type).delete()
1181        Permission._meta.default_permissions = []
1182        create_permissions(self.app_config, verbosity=0)
1183
1184        # custom permission only since default permissions is empty
1185        self.assertEqual(
1186            Permission.objects.filter(content_type=permission_content_type,).count(), 1
1187        )
1188
1189    def test_unavailable_models(self):
1190        """
1191        #24075 - Permissions shouldn't be created or deleted if the ContentType
1192        or Permission models aren't available.
1193        """
1194        state = migrations.state.ProjectState()
1195        # Unavailable contenttypes.ContentType
1196        with self.assertNumQueries(0):
1197            create_permissions(self.app_config, verbosity=0, apps=state.apps)
1198        # Unavailable auth.Permission
1199        state = migrations.state.ProjectState(real_apps=["contenttypes"])
1200        with self.assertNumQueries(0):
1201            create_permissions(self.app_config, verbosity=0, apps=state.apps)
1202
1203    def test_create_permissions_checks_contenttypes_created(self):
1204        """
1205        `post_migrate` handler ordering isn't guaranteed. Simulate a case
1206        where create_permissions() is called before create_contenttypes().
1207        """
1208        # Warm the manager cache.
1209        ContentType.objects.get_for_model(Group)
1210        # Apply a deletion as if e.g. a database 'flush' had been executed.
1211        ContentType.objects.filter(app_label="auth", model="group").delete()
1212        # This fails with a foreign key constraint without the fix.
1213        create_permissions(apps.get_app_config("auth"), interactive=False, verbosity=0)
1214
1215    def test_permission_with_proxy_content_type_created(self):
1216        """
1217        A proxy model's permissions use its own content type rather than the
1218        content type of the concrete model.
1219        """
1220        opts = UserProxy._meta
1221        codename = get_permission_codename("add", opts)
1222        self.assertTrue(
1223            Permission.objects.filter(
1224                content_type__model=opts.model_name,
1225                content_type__app_label=opts.app_label,
1226                codename=codename,
1227            ).exists()
1228        )

tests/auth_tests/test_checks.py

  1from django.contrib.auth.checks import (
  2    check_models_permissions,
  3    check_user_model,
  4)
  5from django.contrib.auth.models import AbstractBaseUser
  6from django.core import checks
  7from django.db import models
  8from django.test import (
  9    SimpleTestCase,
 10    override_settings,
 11    override_system_checks,
 12)
 13from django.test.utils import isolate_apps
 14
 15from .models import CustomUserNonUniqueUsername
 16
 17
 18@isolate_apps("auth_tests", attr_name="apps")
 19@override_system_checks([check_user_model])
 20class UserModelChecksTests(SimpleTestCase):
 21    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonListRequiredFields")
 22    def test_required_fields_is_list(self):
 23        """REQUIRED_FIELDS should be a list."""
 24
 25        class CustomUserNonListRequiredFields(AbstractBaseUser):
 26            username = models.CharField(max_length=30, unique=True)
 27            date_of_birth = models.DateField()
 28
 29            USERNAME_FIELD = "username"
 30            REQUIRED_FIELDS = "date_of_birth"
 31
 32        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
 33        self.assertEqual(
 34            errors,
 35            [
 36                checks.Error(
 37                    "'REQUIRED_FIELDS' must be a list or tuple.",
 38                    obj=CustomUserNonListRequiredFields,
 39                    id="auth.E001",
 40                ),
 41            ],
 42        )
 43
 44    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserBadRequiredFields")
 45    def test_username_not_in_required_fields(self):
 46        """USERNAME_FIELD should not appear in REQUIRED_FIELDS."""
 47
 48        class CustomUserBadRequiredFields(AbstractBaseUser):
 49            username = models.CharField(max_length=30, unique=True)
 50            date_of_birth = models.DateField()
 51
 52            USERNAME_FIELD = "username"
 53            REQUIRED_FIELDS = ["username", "date_of_birth"]
 54
 55        errors = checks.run_checks(self.apps.get_app_configs())
 56        self.assertEqual(
 57            errors,
 58            [
 59                checks.Error(
 60                    "The field named as the 'USERNAME_FIELD' for a custom user model "
 61                    "must not be included in 'REQUIRED_FIELDS'.",
 62                    obj=CustomUserBadRequiredFields,
 63                    id="auth.E002",
 64                ),
 65            ],
 66        )
 67
 68    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonUniqueUsername")
 69    def test_username_non_unique(self):
 70        """
 71        A non-unique USERNAME_FIELD raises an error only if the default
 72        authentication backend is used. Otherwise, a warning is raised.
 73        """
 74        errors = checks.run_checks()
 75        self.assertEqual(
 76            errors,
 77            [
 78                checks.Error(
 79                    "'CustomUserNonUniqueUsername.username' must be "
 80                    "unique because it is named as the 'USERNAME_FIELD'.",
 81                    obj=CustomUserNonUniqueUsername,
 82                    id="auth.E003",
 83                ),
 84            ],
 85        )
 86        with self.settings(AUTHENTICATION_BACKENDS=["my.custom.backend"]):
 87            errors = checks.run_checks()
 88            self.assertEqual(
 89                errors,
 90                [
 91                    checks.Warning(
 92                        "'CustomUserNonUniqueUsername.username' is named as "
 93                        "the 'USERNAME_FIELD', but it is not unique.",
 94                        hint="Ensure that your authentication backend(s) can handle non-unique usernames.",
 95                        obj=CustomUserNonUniqueUsername,
 96                        id="auth.W004",
 97                    ),
 98                ],
 99            )
100
101    @override_settings(AUTH_USER_MODEL="auth_tests.BadUser")
102    def test_is_anonymous_authenticated_methods(self):
103        """
104        <User Model>.is_anonymous/is_authenticated must not be methods.
105        """
106
107        class BadUser(AbstractBaseUser):
108            username = models.CharField(max_length=30, unique=True)
109            USERNAME_FIELD = "username"
110
111            def is_anonymous(self):
112                return True
113
114            def is_authenticated(self):
115                return True
116
117        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
118        self.assertEqual(
119            errors,
120            [
121                checks.Critical(
122                    "%s.is_anonymous must be an attribute or property rather than "
123                    "a method. Ignoring this is a security issue as anonymous "
124                    "users will be treated as authenticated!" % BadUser,
125                    obj=BadUser,
126                    id="auth.C009",
127                ),
128                checks.Critical(
129                    "%s.is_authenticated must be an attribute or property rather "
130                    "than a method. Ignoring this is a security issue as anonymous "
131                    "users will be treated as authenticated!" % BadUser,
132                    obj=BadUser,
133                    id="auth.C010",
134                ),
135            ],
136        )
137
138
139@isolate_apps("auth_tests", attr_name="apps")
140@override_system_checks([check_models_permissions])
141class ModelsPermissionsChecksTests(SimpleTestCase):
142    def test_clashing_default_permissions(self):
143        class Checked(models.Model):
144            class Meta:
145                permissions = [("change_checked", "Can edit permission (duplicate)")]
146
147        errors = checks.run_checks(self.apps.get_app_configs())
148        self.assertEqual(
149            errors,
150            [
151                checks.Error(
152                    "The permission codenamed 'change_checked' clashes with a builtin "
153                    "permission for model 'auth_tests.Checked'.",
154                    obj=Checked,
155                    id="auth.E005",
156                ),
157            ],
158        )
159
160    def test_non_clashing_custom_permissions(self):
161        class Checked(models.Model):
162            class Meta:
163                permissions = [
164                    ("my_custom_permission", "Some permission"),
165                    ("other_one", "Some other permission"),
166                ]
167
168        errors = checks.run_checks(self.apps.get_app_configs())
169        self.assertEqual(errors, [])
170
171    def test_clashing_custom_permissions(self):
172        class Checked(models.Model):
173            class Meta:
174                permissions = [
175                    ("my_custom_permission", "Some permission"),
176                    ("other_one", "Some other permission"),
177                    (
178                        "my_custom_permission",
179                        "Some permission with duplicate permission code",
180                    ),
181                ]
182
183        errors = checks.run_checks(self.apps.get_app_configs())
184        self.assertEqual(
185            errors,
186            [
187                checks.Error(
188                    "The permission codenamed 'my_custom_permission' is duplicated for "
189                    "model 'auth_tests.Checked'.",
190                    obj=Checked,
191                    id="auth.E006",
192                ),
193            ],
194        )
195
196    def test_verbose_name_max_length(self):
197        class Checked(models.Model):
198            class Meta:
199                verbose_name = (
200                    "some ridiculously long verbose name that is out of control" * 5
201                )
202
203        errors = checks.run_checks(self.apps.get_app_configs())
204        self.assertEqual(
205            errors,
206            [
207                checks.Error(
208                    "The verbose_name of model 'auth_tests.Checked' must be at most 244 "
209                    "characters for its builtin permission names to be at most 255 characters.",
210                    obj=Checked,
211                    id="auth.E007",
212                ),
213            ],
214        )
215
216    def test_custom_permission_name_max_length(self):
217        custom_permission_name = (
218            "some ridiculously long verbose name that is out of control" * 5
219        )
220
221        class Checked(models.Model):
222            class Meta:
223                permissions = [
224                    ("my_custom_permission", custom_permission_name),
225                ]
226
227        errors = checks.run_checks(self.apps.get_app_configs())
228        self.assertEqual(
229            errors,
230            [
231                checks.Error(
232                    "The permission named '%s' of model 'auth_tests.Checked' is longer "
233                    "than 255 characters." % custom_permission_name,
234                    obj=Checked,
235                    id="auth.E008",
236                ),
237            ],
238        )
239
240    def test_empty_default_permissions(self):
241        class Checked(models.Model):
242            class Meta:
243                default_permissions = ()
244
245        self.assertEqual(checks.run_checks(self.apps.get_app_configs()), [])

tests/auth_tests/models/minimal.py

1from django.db import models
2
3
4class MinimalUser(models.Model):
5    REQUIRED_FIELDS = ()
6    USERNAME_FIELD = "id"

tests/auth_tests/models/custom_permissions.py

 1"""
 2The CustomPermissionsUser users email as the identifier, but uses the normal
 3Django permissions model. This allows us to check that the PermissionsMixin
 4includes everything that is needed to interact with the ModelBackend.
 5"""
 6from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
 7from django.db import models
 8
 9from .custom_user import CustomUserManager, RemoveGroupsAndPermissions
10
11
12class CustomPermissionsUserManager(CustomUserManager):
13    def create_superuser(self, email, password, date_of_birth):
14        u = self.create_user(email, password=password, date_of_birth=date_of_birth)
15        u.is_superuser = True
16        u.save(using=self._db)
17        return u
18
19
20with RemoveGroupsAndPermissions():
21
22    class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin):
23        email = models.EmailField(
24            verbose_name="email address", max_length=255, unique=True
25        )
26        date_of_birth = models.DateField()
27
28        custom_objects = CustomPermissionsUserManager()
29
30        USERNAME_FIELD = "email"
31        REQUIRED_FIELDS = ["date_of_birth"]
32
33        def __str__(self):
34            return self.email

tests/auth_tests/models/with_integer_username.py

 1from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
 2from django.db import models
 3
 4
 5class IntegerUsernameUserManager(BaseUserManager):
 6    def create_user(self, username, password):
 7        user = self.model(username=username)
 8        user.set_password(password)
 9        user.save(using=self._db)
10        return user
11
12    def get_by_natural_key(self, username):
13        return self.get(username=username)
14
15
16class IntegerUsernameUser(AbstractBaseUser):
17    username = models.IntegerField()
18    password = models.CharField(max_length=255)
19
20    USERNAME_FIELD = "username"
21    REQUIRED_FIELDS = ["username", "password"]
22
23    objects = IntegerUsernameUserManager()

tests/auth_tests/models/with_many_to_many.py

 1from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
 2from django.db import models
 3
 4
 5class Organization(models.Model):
 6    name = models.CharField(max_length=255)
 7
 8
 9class CustomUserWithM2MManager(BaseUserManager):
10    def create_superuser(self, username, orgs, password):
11        user = self.model(username=username)
12        user.set_password(password)
13        user.save(using=self._db)
14        user.orgs.add(*orgs)
15        return user
16
17
18class CustomUserWithM2M(AbstractBaseUser):
19    username = models.CharField(max_length=30, unique=True)
20    orgs = models.ManyToManyField(Organization)
21
22    custom_objects = CustomUserWithM2MManager()
23
24    USERNAME_FIELD = "username"
25    REQUIRED_FIELDS = ["orgs"]
26
27
28class CustomUserWithM2MThrough(AbstractBaseUser):
29    username = models.CharField(max_length=30, unique=True)
30    orgs = models.ManyToManyField(Organization, through="Membership")
31
32    custom_objects = CustomUserWithM2MManager()
33
34    USERNAME_FIELD = "username"
35    REQUIRED_FIELDS = ["orgs"]
36
37
38class Membership(models.Model):
39    user = models.ForeignKey(CustomUserWithM2MThrough, on_delete=models.CASCADE)
40    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)

tests/auth_tests/models/custom_user.py

  1from django.contrib.auth.models import (
  2    AbstractBaseUser,
  3    AbstractUser,
  4    BaseUserManager,
  5    Group,
  6    Permission,
  7    PermissionsMixin,
  8    UserManager,
  9)
 10from django.db import models
 11
 12
 13# The custom user uses email as the unique identifier, and requires
 14# that every user provide a date of birth. This lets us test
 15# changes in username datatype, and non-text required fields.
 16class CustomUserManager(BaseUserManager):
 17    def create_user(self, email, date_of_birth, password=None, **fields):
 18        """
 19        Creates and saves a User with the given email and password.
 20        """
 21        if not email:
 22            raise ValueError("Users must have an email address")
 23
 24        user = self.model(
 25            email=self.normalize_email(email), date_of_birth=date_of_birth, **fields
 26        )
 27
 28        user.set_password(password)
 29        user.save(using=self._db)
 30        return user
 31
 32    def create_superuser(self, email, password, date_of_birth, **fields):
 33        u = self.create_user(
 34            email, password=password, date_of_birth=date_of_birth, **fields
 35        )
 36        u.is_admin = True
 37        u.save(using=self._db)
 38        return u
 39
 40
 41class CustomUser(AbstractBaseUser):
 42    email = models.EmailField(verbose_name="email address", max_length=255, unique=True)
 43    is_active = models.BooleanField(default=True)
 44    is_admin = models.BooleanField(default=False)
 45    date_of_birth = models.DateField()
 46    first_name = models.CharField(max_length=50)
 47
 48    custom_objects = CustomUserManager()
 49
 50    USERNAME_FIELD = "email"
 51    REQUIRED_FIELDS = ["date_of_birth", "first_name"]
 52
 53    def __str__(self):
 54        return self.email
 55
 56    # Maybe required?
 57    def get_group_permissions(self, obj=None):
 58        return set()
 59
 60    def get_all_permissions(self, obj=None):
 61        return set()
 62
 63    def has_perm(self, perm, obj=None):
 64        return True
 65
 66    def has_perms(self, perm_list, obj=None):
 67        return True
 68
 69    def has_module_perms(self, app_label):
 70        return True
 71
 72    # Admin required fields
 73    @property
 74    def is_staff(self):
 75        return self.is_admin
 76
 77
 78class RemoveGroupsAndPermissions:
 79    """
 80    A context manager to temporarily remove the groups and user_permissions M2M
 81    fields from the AbstractUser class, so they don't clash with the
 82    related_name sets.
 83    """
 84
 85    def __enter__(self):
 86        self._old_au_local_m2m = AbstractUser._meta.local_many_to_many
 87        self._old_pm_local_m2m = PermissionsMixin._meta.local_many_to_many
 88        groups = models.ManyToManyField(Group, blank=True)
 89        groups.contribute_to_class(PermissionsMixin, "groups")
 90        user_permissions = models.ManyToManyField(Permission, blank=True)
 91        user_permissions.contribute_to_class(PermissionsMixin, "user_permissions")
 92        PermissionsMixin._meta.local_many_to_many = [groups, user_permissions]
 93        AbstractUser._meta.local_many_to_many = [groups, user_permissions]
 94
 95    def __exit__(self, exc_type, exc_value, traceback):
 96        AbstractUser._meta.local_many_to_many = self._old_au_local_m2m
 97        PermissionsMixin._meta.local_many_to_many = self._old_pm_local_m2m
 98
 99
100class CustomUserWithoutIsActiveField(AbstractBaseUser):
101    username = models.CharField(max_length=150, unique=True)
102    email = models.EmailField(unique=True)
103
104    objects = UserManager()
105
106    USERNAME_FIELD = "username"
107
108
109# The extension user is a simple extension of the built-in user class,
110# adding a required date_of_birth field. This allows us to check for
111# any hard references to the name "User" in forms/handlers etc.
112with RemoveGroupsAndPermissions():
113
114    class ExtensionUser(AbstractUser):
115        date_of_birth = models.DateField()
116
117        custom_objects = UserManager()
118
119        REQUIRED_FIELDS = AbstractUser.REQUIRED_FIELDS + ["date_of_birth"]

tests/auth_tests/models/invalid_models.py

 1from django.contrib.auth.models import AbstractBaseUser, UserManager
 2from django.db import models
 3
 4
 5class CustomUserNonUniqueUsername(AbstractBaseUser):
 6    """
 7    A user with a non-unique username.
 8
 9    This model is not invalid if it is used with a custom authentication
10    backend which supports non-unique usernames.
11    """
12
13    username = models.CharField(max_length=30)
14    email = models.EmailField(blank=True)
15    is_staff = models.BooleanField(default=False)
16    is_superuser = models.BooleanField(default=False)
17
18    USERNAME_FIELD = "username"
19    REQUIRED_FIELDS = ["email"]
20
21    objects = UserManager()

tests/auth_tests/models/with_foreign_key.py

  1from django.contrib.auth.checks import (
  2    check_models_permissions,
  3    check_user_model,
  4)
  5from django.contrib.auth.models import AbstractBaseUser
  6from django.core import checks
  7from django.db import models
  8from django.test import (
  9    SimpleTestCase,
 10    override_settings,
 11    override_system_checks,
 12)
 13from django.test.utils import isolate_apps
 14
 15from .models import CustomUserNonUniqueUsername
 16
 17
 18@isolate_apps("auth_tests", attr_name="apps")
 19@override_system_checks([check_user_model])
 20class UserModelChecksTests(SimpleTestCase):
 21    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonListRequiredFields")
 22    def test_required_fields_is_list(self):
 23        """REQUIRED_FIELDS should be a list."""
 24
 25        class CustomUserNonListRequiredFields(AbstractBaseUser):
 26            username = models.CharField(max_length=30, unique=True)
 27            date_of_birth = models.DateField()
 28
 29            USERNAME_FIELD = "username"
 30            REQUIRED_FIELDS = "date_of_birth"
 31
 32        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
 33        self.assertEqual(
 34            errors,
 35            [
 36                checks.Error(
 37                    "'REQUIRED_FIELDS' must be a list or tuple.",
 38                    obj=CustomUserNonListRequiredFields,
 39                    id="auth.E001",
 40                ),
 41            ],
 42        )
 43
 44    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserBadRequiredFields")
 45    def test_username_not_in_required_fields(self):
 46        """USERNAME_FIELD should not appear in REQUIRED_FIELDS."""
 47
 48        class CustomUserBadRequiredFields(AbstractBaseUser):
 49            username = models.CharField(max_length=30, unique=True)
 50            date_of_birth = models.DateField()
 51
 52            USERNAME_FIELD = "username"
 53            REQUIRED_FIELDS = ["username", "date_of_birth"]
 54
 55        errors = checks.run_checks(self.apps.get_app_configs())
 56        self.assertEqual(
 57            errors,
 58            [
 59                checks.Error(
 60                    "The field named as the 'USERNAME_FIELD' for a custom user model "
 61                    "must not be included in 'REQUIRED_FIELDS'.",
 62                    obj=CustomUserBadRequiredFields,
 63                    id="auth.E002",
 64                ),
 65            ],
 66        )
 67
 68    @override_settings(AUTH_USER_MODEL="auth_tests.CustomUserNonUniqueUsername")
 69    def test_username_non_unique(self):
 70        """
 71        A non-unique USERNAME_FIELD raises an error only if the default
 72        authentication backend is used. Otherwise, a warning is raised.
 73        """
 74        errors = checks.run_checks()
 75        self.assertEqual(
 76            errors,
 77            [
 78                checks.Error(
 79                    "'CustomUserNonUniqueUsername.username' must be "
 80                    "unique because it is named as the 'USERNAME_FIELD'.",
 81                    obj=CustomUserNonUniqueUsername,
 82                    id="auth.E003",
 83                ),
 84            ],
 85        )
 86        with self.settings(AUTHENTICATION_BACKENDS=["my.custom.backend"]):
 87            errors = checks.run_checks()
 88            self.assertEqual(
 89                errors,
 90                [
 91                    checks.Warning(
 92                        "'CustomUserNonUniqueUsername.username' is named as "
 93                        "the 'USERNAME_FIELD', but it is not unique.",
 94                        hint="Ensure that your authentication backend(s) can handle non-unique usernames.",
 95                        obj=CustomUserNonUniqueUsername,
 96                        id="auth.W004",
 97                    ),
 98                ],
 99            )
100
101    @override_settings(AUTH_USER_MODEL="auth_tests.BadUser")
102    def test_is_anonymous_authenticated_methods(self):
103        """
104        <User Model>.is_anonymous/is_authenticated must not be methods.
105        """
106
107        class BadUser(AbstractBaseUser):
108            username = models.CharField(max_length=30, unique=True)
109            USERNAME_FIELD = "username"
110
111            def is_anonymous(self):
112                return True
113
114            def is_authenticated(self):
115                return True
116
117        errors = checks.run_checks(app_configs=self.apps.get_app_configs())
118        self.assertEqual(
119            errors,
120            [
121                checks.Critical(
122                    "%s.is_anonymous must be an attribute or property rather than "
123                    "a method. Ignoring this is a security issue as anonymous "
124                    "users will be treated as authenticated!" % BadUser,
125                    obj=BadUser,
126                    id="auth.C009",
127                ),
128                checks.Critical(
129                    "%s.is_authenticated must be an attribute or property rather "
130                    "than a method. Ignoring this is a security issue as anonymous "
131                    "users will be treated as authenticated!" % BadUser,
132                    obj=BadUser,
133                    id="auth.C010",
134                ),
135            ],
136        )
137
138
139@isolate_apps("auth_tests", attr_name="apps")
140@override_system_checks([check_models_permissions])
141class ModelsPermissionsChecksTests(SimpleTestCase):
142    def test_clashing_default_permissions(self):
143        class Checked(models.Model):
144            class Meta:
145                permissions = [("change_checked", "Can edit permission (duplicate)")]
146
147        errors = checks.run_checks(self.apps.get_app_configs())
148        self.assertEqual(
149            errors,
150            [
151                checks.Error(
152                    "The permission codenamed 'change_checked' clashes with a builtin "
153                    "permission for model 'auth_tests.Checked'.",
154                    obj=Checked,
155                    id="auth.E005",
156                ),
157            ],
158        )
159
160    def test_non_clashing_custom_permissions(self):
161        class Checked(models.Model):
162            class Meta:
163                permissions = [
164                    ("my_custom_permission", "Some permission"),
165                    ("other_one", "Some other permission"),
166                ]
167
168        errors = checks.run_checks(self.apps.get_app_configs())
169        self.assertEqual(errors, [])
170
171    def test_clashing_custom_permissions(self):
172        class Checked(models.Model):
173            class Meta:
174                permissions = [
175                    ("my_custom_permission", "Some permission"),
176                    ("other_one", "Some other permission"),
177                    (
178                        "my_custom_permission",
179                        "Some permission with duplicate permission code",
180                    ),
181                ]
182
183        errors = checks.run_checks(self.apps.get_app_configs())
184        self.assertEqual(
185            errors,
186            [
187                checks.Error(
188                    "The permission codenamed 'my_custom_permission' is duplicated for "
189                    "model 'auth_tests.Checked'.",
190                    obj=Checked,
191                    id="auth.E006",
192                ),
193            ],
194        )
195
196    def test_verbose_name_max_length(self):
197        class Checked(models.Model):
198            class Meta:
199                verbose_name = (
200                    "some ridiculously long verbose name that is out of control" * 5
201                )
202
203        errors = checks.run_checks(self.apps.get_app_configs())
204        self.assertEqual(
205            errors,
206            [
207                checks.Error(
208                    "The verbose_name of model 'auth_tests.Checked' must be at most 244 "
209                    "characters for its builtin permission names to be at most 255 characters.",
210                    obj=Checked,
211                    id="auth.E007",
212                ),
213            ],
214        )
215
216    def test_custom_permission_name_max_length(self):
217        custom_permission_name = (
218            "some ridiculously long verbose name that is out of control" * 5
219        )
220
221        class Checked(models.Model):
222            class Meta:
223                permissions = [
224                    ("my_custom_permission", custom_permission_name),
225                ]
226
227        errors = checks.run_checks(self.apps.get_app_configs())
228        self.assertEqual(
229            errors,
230            [
231                checks.Error(
232                    "The permission named '%s' of model 'auth_tests.Checked' is longer "
233                    "than 255 characters." % custom_permission_name,
234                    obj=Checked,
235                    id="auth.E008",
236                ),
237            ],
238        )
239
240    def test_empty_default_permissions(self):
241        class Checked(models.Model):
242            class Meta:
243                default_permissions = ()
244
245        self.assertEqual(checks.run_checks(self.apps.get_app_configs()), [])

tests/auth_tests/test_hashers.py

  1from unittest import mock, skipUnless
  2
  3from django.conf.global_settings import PASSWORD_HASHERS
  4from django.contrib.auth.hashers import (
  5    UNUSABLE_PASSWORD_PREFIX,
  6    UNUSABLE_PASSWORD_SUFFIX_LENGTH,
  7    BasePasswordHasher,
  8    PBKDF2PasswordHasher,
  9    PBKDF2SHA1PasswordHasher,
 10    check_password,
 11    get_hasher,
 12    identify_hasher,
 13    is_password_usable,
 14    make_password,
 15)
 16from django.test import SimpleTestCase
 17from django.test.utils import override_settings
 18
 19try:
 20    import crypt
 21except ImportError:
 22    crypt = None
 23else:
 24    # On some platforms (e.g. OpenBSD), crypt.crypt() always return None.
 25    if crypt.crypt("", "") is None:
 26        crypt = None
 27
 28try:
 29    import bcrypt
 30except ImportError:
 31    bcrypt = None
 32
 33try:
 34    import argon2
 35except ImportError:
 36    argon2 = None
 37
 38
 39class PBKDF2SingleIterationHasher(PBKDF2PasswordHasher):
 40    iterations = 1
 41
 42
 43@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
 44class TestUtilsHashPass(SimpleTestCase):
 45    def test_simple(self):
 46        encoded = make_password("lètmein")
 47        self.assertTrue(encoded.startswith("pbkdf2_sha256$"))
 48        self.assertTrue(is_password_usable(encoded))
 49        self.assertTrue(check_password("lètmein", encoded))
 50        self.assertFalse(check_password("lètmeinz", encoded))
 51        # Blank passwords
 52        blank_encoded = make_password("")
 53        self.assertTrue(blank_encoded.startswith("pbkdf2_sha256$"))
 54        self.assertTrue(is_password_usable(blank_encoded))
 55        self.assertTrue(check_password("", blank_encoded))
 56        self.assertFalse(check_password(" ", blank_encoded))
 57
 58    def test_pbkdf2(self):
 59        encoded = make_password("lètmein", "seasalt", "pbkdf2_sha256")
 60        self.assertEqual(
 61            encoded,
 62            "pbkdf2_sha256$216000$seasalt$youGZxOw6ZOcfrXv2i8/AhrnpZflJJ9EshS9XmUJTUg=",
 63        )
 64        self.assertTrue(is_password_usable(encoded))
 65        self.assertTrue(check_password("lètmein", encoded))
 66        self.assertFalse(check_password("lètmeinz", encoded))
 67        self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
 68        # Blank passwords
 69        blank_encoded = make_password("", "seasalt", "pbkdf2_sha256")
 70        self.assertTrue(blank_encoded.startswith("pbkdf2_sha256$"))
 71        self.assertTrue(is_password_usable(blank_encoded))
 72        self.assertTrue(check_password("", blank_encoded))
 73        self.assertFalse(check_password(" ", blank_encoded))
 74
 75    @override_settings(
 76        PASSWORD_HASHERS=["django.contrib.auth.hashers.SHA1PasswordHasher"]
 77    )
 78    def test_sha1(self):
 79        encoded = make_password("lètmein", "seasalt", "sha1")
 80        self.assertEqual(
 81            encoded, "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8"
 82        )
 83        self.assertTrue(is_password_usable(encoded))
 84        self.assertTrue(check_password("lètmein", encoded))
 85        self.assertFalse(check_password("lètmeinz", encoded))
 86        self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
 87        # Blank passwords
 88        blank_encoded = make_password("", "seasalt", "sha1")
 89        self.assertTrue(blank_encoded.startswith("sha1$"))
 90        self.assertTrue(is_password_usable(blank_encoded))
 91        self.assertTrue(check_password("", blank_encoded))
 92        self.assertFalse(check_password(" ", blank_encoded))
 93
 94    @override_settings(
 95        PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"]
 96    )
 97    def test_md5(self):
 98        encoded = make_password("lètmein", "seasalt", "md5")
 99        self.assertEqual(encoded, "md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3")
100        self.assertTrue(is_password_usable(encoded))
101        self.assertTrue(check_password("lètmein", encoded))
102        self.assertFalse(check_password("lètmeinz", encoded))
103        self.assertEqual(identify_hasher(encoded).algorithm, "md5")
104        # Blank passwords
105        blank_encoded = make_password("", "seasalt", "md5")
106        self.assertTrue(blank_encoded.startswith("md5$"))
107        self.assertTrue(is_password_usable(blank_encoded))
108        self.assertTrue(check_password("", blank_encoded))
109        self.assertFalse(check_password(" ", blank_encoded))
110
111    @override_settings(
112        PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedMD5PasswordHasher"]
113    )
114    def test_unsalted_md5(self):
115        encoded = make_password("lètmein", "", "unsalted_md5")
116        self.assertEqual(encoded, "88a434c88cca4e900f7874cd98123f43")
117        self.assertTrue(is_password_usable(encoded))
118        self.assertTrue(check_password("lètmein", encoded))
119        self.assertFalse(check_password("lètmeinz", encoded))
120        self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_md5")
121        # Alternate unsalted syntax
122        alt_encoded = "md5$$%s" % encoded
123        self.assertTrue(is_password_usable(alt_encoded))
124        self.assertTrue(check_password("lètmein", alt_encoded))
125        self.assertFalse(check_password("lètmeinz", alt_encoded))
126        # Blank passwords
127        blank_encoded = make_password("", "", "unsalted_md5")
128        self.assertTrue(is_password_usable(blank_encoded))
129        self.assertTrue(check_password("", blank_encoded))
130        self.assertFalse(check_password(" ", blank_encoded))
131
132    @override_settings(
133        PASSWORD_HASHERS=["django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"]
134    )
135    def test_unsalted_sha1(self):
136        encoded = make_password("lètmein", "", "unsalted_sha1")
137        self.assertEqual(encoded, "sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b")
138        self.assertTrue(is_password_usable(encoded))
139        self.assertTrue(check_password("lètmein", encoded))
140        self.assertFalse(check_password("lètmeinz", encoded))
141        self.assertEqual(identify_hasher(encoded).algorithm, "unsalted_sha1")
142        # Raw SHA1 isn't acceptable
143        alt_encoded = encoded[6:]
144        self.assertFalse(check_password("lètmein", alt_encoded))
145        # Blank passwords
146        blank_encoded = make_password("", "", "unsalted_sha1")
147        self.assertTrue(blank_encoded.startswith("sha1$"))
148        self.assertTrue(is_password_usable(blank_encoded))
149        self.assertTrue(check_password("", blank_encoded))
150        self.assertFalse(check_password(" ", blank_encoded))
151
152    @skipUnless(crypt, "no crypt module to generate password.")
153    @override_settings(
154        PASSWORD_HASHERS=["django.contrib.auth.hashers.CryptPasswordHasher"]
155    )
156    def test_crypt(self):
157        encoded = make_password("lètmei", "ab", "crypt")
158        self.assertEqual(encoded, "crypt$$ab1Hv2Lg7ltQo")
159        self.assertTrue(is_password_usable(encoded))
160        self.assertTrue(check_password("lètmei", encoded))
161        self.assertFalse(check_password("lètmeiz", encoded))
162        self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
163        # Blank passwords
164        blank_encoded = make_password("", "ab", "crypt")
165        self.assertTrue(blank_encoded.startswith("crypt$"))
166        self.assertTrue(is_password_usable(blank_encoded))
167        self.assertTrue(check_password("", blank_encoded))
168        self.assertFalse(check_password(" ", blank_encoded))
169
170    @skipUnless(bcrypt, "bcrypt not installed")
171    def test_bcrypt_sha256(self):
172        encoded = make_password("lètmein", hasher="bcrypt_sha256")
173        self.assertTrue(is_password_usable(encoded))
174        self.assertTrue(encoded.startswith("bcrypt_sha256$"))
175        self.assertTrue(check_password("lètmein", encoded))
176        self.assertFalse(check_password("lètmeinz", encoded))
177        self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt_sha256")
178
179        # password truncation no longer works
180        password = (
181            "VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5"
182            "JGB3RGZ7VI7G7DJQ9NI8BQFSRPTG6UWTTVESA5ZPUN"
183        )
184        encoded = make_password(password, hasher="bcrypt_sha256")
185        self.assertTrue(check_password(password, encoded))
186        self.assertFalse(check_password(password[:72], encoded))
187        # Blank passwords
188        blank_encoded = make_password("", hasher="bcrypt_sha256")
189        self.assertTrue(blank_encoded.startswith("bcrypt_sha256$"))
190        self.assertTrue(is_password_usable(blank_encoded))
191        self.assertTrue(check_password("", blank_encoded))
192        self.assertFalse(check_password(" ", blank_encoded))
193
194    @skipUnless(bcrypt, "bcrypt not installed")
195    @override_settings(
196        PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
197    )
198    def test_bcrypt(self):
199        encoded = make_password("lètmein", hasher="bcrypt")
200        self.assertTrue(is_password_usable(encoded))
201        self.assertTrue(encoded.startswith("bcrypt$"))
202        self.assertTrue(check_password("lètmein", encoded))
203        self.assertFalse(check_password("lètmeinz", encoded))
204        self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
205        # Blank passwords
206        blank_encoded = make_password("", hasher="bcrypt")
207        self.assertTrue(blank_encoded.startswith("bcrypt$"))
208        self.assertTrue(is_password_usable(blank_encoded))
209        self.assertTrue(check_password("", blank_encoded))
210        self.assertFalse(check_password(" ", blank_encoded))
211
212    @skipUnless(bcrypt, "bcrypt not installed")
213    @override_settings(
214        PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
215    )
216    def test_bcrypt_upgrade(self):
217        hasher = get_hasher("bcrypt")
218        self.assertEqual("bcrypt", hasher.algorithm)
219        self.assertNotEqual(hasher.rounds, 4)
220
221        old_rounds = hasher.rounds
222        try:
223            # Generate a password with 4 rounds.
224            hasher.rounds = 4
225            encoded = make_password("letmein", hasher="bcrypt")
226            rounds = hasher.safe_summary(encoded)["work factor"]
227            self.assertEqual(rounds, "04")
228
229            state = {"upgraded": False}
230
231            def setter(password):
232                state["upgraded"] = True
233
234            # No upgrade is triggered.
235            self.assertTrue(check_password("letmein", encoded, setter, "bcrypt"))
236            self.assertFalse(state["upgraded"])
237
238            # Revert to the old rounds count and ...
239            hasher.rounds = old_rounds
240
241            # ... check if the password would get updated to the new count.
242            self.assertTrue(check_password("letmein", encoded, setter, "bcrypt"))
243            self.assertTrue(state["upgraded"])
244        finally:
245            hasher.rounds = old_rounds
246
247    @skipUnless(bcrypt, "bcrypt not installed")
248    @override_settings(
249        PASSWORD_HASHERS=["django.contrib.auth.hashers.BCryptPasswordHasher"]
250    )
251    def test_bcrypt_harden_runtime(self):
252        hasher = get_hasher("bcrypt")
253        self.assertEqual("bcrypt", hasher.algorithm)
254
255        with mock.patch.object(hasher, "rounds", 4):
256            encoded = make_password("letmein", hasher="bcrypt")
257
258        with mock.patch.object(hasher, "rounds", 6), mock.patch.object(
259            hasher, "encode", side_effect=hasher.encode
260        ):
261            hasher.harden_runtime("wrong_password", encoded)
262
263            # Increasing rounds from 4 to 6 means an increase of 4 in workload,
264            # therefore hardening should run 3 times to make the timing the
265            # same (the original encode() call already ran once).
266            self.assertEqual(hasher.encode.call_count, 3)
267
268            # Get the original salt (includes the original workload factor)
269            algorithm, data = encoded.split("$", 1)
270            expected_call = (("wrong_password", data[:29].encode()),)
271            self.assertEqual(hasher.encode.call_args_list, [expected_call] * 3)
272
273    def test_unusable(self):
274        encoded = make_password(None)
275        self.assertEqual(
276            len(encoded),
277            len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH,
278        )
279        self.assertFalse(is_password_usable(encoded))
280        self.assertFalse(check_password(None, encoded))
281        self.assertFalse(check_password(encoded, encoded))
282        self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
283        self.assertFalse(check_password("", encoded))
284        self.assertFalse(check_password("lètmein", encoded))
285        self.assertFalse(check_password("lètmeinz", encoded))
286        with self.assertRaisesMessage(ValueError, "Unknown password hashing algorith"):
287            identify_hasher(encoded)
288        # Assert that the unusable passwords actually contain a random part.
289        # This might fail one day due to a hash collision.
290        self.assertNotEqual(encoded, make_password(None), "Random password collision?")
291
292    def test_unspecified_password(self):
293        """
294        Makes sure specifying no plain password with a valid encoded password
295        returns `False`.
296        """
297        self.assertFalse(check_password(None, make_password("lètmein")))
298
299    def test_bad_algorithm(self):
300        msg = (
301            "Unknown password hashing algorithm '%s'. Did you specify it in "
302            "the PASSWORD_HASHERS setting?"
303        )
304        with self.assertRaisesMessage(ValueError, msg % "lolcat"):
305            make_password("lètmein", hasher="lolcat")
306        with self.assertRaisesMessage(ValueError, msg % "lolcat"):
307            identify_hasher("lolcat$salt$hash")
308
309    def test_is_password_usable(self):
310        passwords = ("lètmein_badencoded", "", None)
311        for password in passwords:
312            with self.subTest(password=password):
313                self.assertIs(is_password_usable(password), True)
314
315    def test_low_level_pbkdf2(self):
316        hasher = PBKDF2PasswordHasher()
317        encoded = hasher.encode("lètmein", "seasalt2")
318        self.assertEqual(
319            encoded,
320            "pbkdf2_sha256$216000$seasalt2$gHyszNJ9lwTG5y3MQUjZe+OJmYVTBPl/y7bYq9dtk8M=",
321        )
322        self.assertTrue(hasher.verify("lètmein", encoded))
323
324    def test_low_level_pbkdf2_sha1(self):
325        hasher = PBKDF2SHA1PasswordHasher()
326        encoded = hasher.encode("lètmein", "seasalt2")
327        self.assertEqual(
328            encoded, "pbkdf2_sha1$216000$seasalt2$E1KH89wMKuPXrrQzifVcG4cBtiA="
329        )
330        self.assertTrue(hasher.verify("lètmein", encoded))
331
332    @override_settings(
333        PASSWORD_HASHERS=[
334            "django.contrib.auth.hashers.PBKDF2PasswordHasher",
335            "django.contrib.auth.hashers.SHA1PasswordHasher",
336            "django.contrib.auth.hashers.MD5PasswordHasher",
337        ],
338    )
339    def test_upgrade(self):
340        self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
341        for algo in ("sha1", "md5"):
342            with self.subTest(algo=algo):
343                encoded = make_password("lètmein", hasher=algo)
344                state = {"upgraded": False}
345
346                def setter(password):
347                    state["upgraded"] = True
348
349                self.assertTrue(check_password("lètmein", encoded, setter))
350                self.assertTrue(state["upgraded"])
351
352    def test_no_upgrade(self):
353        encoded = make_password("lètmein")
354        state = {"upgraded": False}
355
356        def setter():
357            state["upgraded"] = True
358
359        self.assertFalse(check_password("WRONG", encoded, setter))
360        self.assertFalse(state["upgraded"])
361
362    @override_settings(
363        PASSWORD_HASHERS=[
364            "django.contrib.auth.hashers.PBKDF2PasswordHasher",
365            "django.contrib.auth.hashers.SHA1PasswordHasher",
366            "django.contrib.auth.hashers.MD5PasswordHasher",
367        ],
368    )
369    def test_no_upgrade_on_incorrect_pass(self):
370        self.assertEqual("pbkdf2_sha256", get_hasher("default").algorithm)
371        for algo in ("sha1", "md5"):
372            with self.subTest(algo=algo):
373                encoded = make_password("lètmein", hasher=algo)
374                state = {"upgraded": False}
375
376                def setter():
377                    state["upgraded"] = True
378
379                self.assertFalse(check_password("WRONG", encoded, setter))
380                self.assertFalse(state["upgraded"])
381
382    def test_pbkdf2_upgrade(self):
383        hasher = get_hasher("default")
384        self.assertEqual("pbkdf2_sha256", hasher.algorithm)
385        self.assertNotEqual(hasher.iterations, 1)
386
387        old_iterations = hasher.iterations
388        try:
389            # Generate a password with 1 iteration.
390            hasher.iterations = 1
391            encoded = make_password("letmein")
392            algo, iterations, salt, hash = encoded.split("$", 3)
393            self.assertEqual(iterations, "1")
394
395            state = {"upgraded": False}
396
397            def setter(password):
398                state["upgraded"] = True
399
400            # No upgrade is triggered
401            self.assertTrue(check_password("letmein", encoded, setter))
402            self.assertFalse(state["upgraded"])
403
404            # Revert to the old iteration count and ...
405            hasher.iterations = old_iterations
406
407            # ... check if the password would get updated to the new iteration count.
408            self.assertTrue(check_password("letmein", encoded, setter))
409            self.assertTrue(state["upgraded"])
410        finally:
411            hasher.iterations = old_iterations
412
413    def test_pbkdf2_harden_runtime(self):
414        hasher = get_hasher("default")
415        self.assertEqual("pbkdf2_sha256", hasher.algorithm)
416
417        with mock.patch.object(hasher, "iterations", 1):
418            encoded = make_password("letmein")
419
420        with mock.patch.object(hasher, "iterations", 6), mock.patch.object(
421            hasher, "encode", side_effect=hasher.encode
422        ):
423            hasher.harden_runtime("wrong_password", encoded)
424
425            # Encode should get called once ...
426            self.assertEqual(hasher.encode.call_count, 1)
427
428            # ... with the original salt and 5 iterations.
429            algorithm, iterations, salt, hash = encoded.split("$", 3)
430            expected_call = (("wrong_password", salt, 5),)
431            self.assertEqual(hasher.encode.call_args, expected_call)
432
433    def test_pbkdf2_upgrade_new_hasher(self):
434        hasher = get_hasher("default")
435        self.assertEqual("pbkdf2_sha256", hasher.algorithm)
436        self.assertNotEqual(hasher.iterations, 1)
437
438        state = {"upgraded": False}
439
440        def setter(password):
441            state["upgraded"] = True
442
443        with self.settings(
444            PASSWORD_HASHERS=["auth_tests.test_hashers.PBKDF2SingleIterationHasher"]
445        ):
446            encoded = make_password("letmein")
447            algo, iterations, salt, hash = encoded.split("$", 3)
448            self.assertEqual(iterations, "1")
449
450            # No upgrade is triggered
451            self.assertTrue(check_password("letmein", encoded, setter))
452            self.assertFalse(state["upgraded"])
453
454        # Revert to the old iteration count and check if the password would get
455        # updated to the new iteration count.
456        with self.settings(
457            PASSWORD_HASHERS=[
458                "django.contrib.auth.hashers.PBKDF2PasswordHasher",
459                "auth_tests.test_hashers.PBKDF2SingleIterationHasher",
460            ]
461        ):
462            self.assertTrue(check_password("letmein", encoded, setter))
463            self.assertTrue(state["upgraded"])
464
465    def test_check_password_calls_harden_runtime(self):
466        hasher = get_hasher("default")
467        encoded = make_password("letmein")
468
469        with mock.patch.object(hasher, "harden_runtime"), mock.patch.object(
470            hasher, "must_update", return_value=True
471        ):
472            # Correct password supplied, no hardening needed
473            check_password("letmein", encoded)
474            self.assertEqual(hasher.harden_runtime.call_count, 0)
475
476            # Wrong password supplied, hardening needed
477            check_password("wrong_password", encoded)
478            self.assertEqual(hasher.harden_runtime.call_count, 1)
479
480
481class BasePasswordHasherTests(SimpleTestCase):
482    not_implemented_msg = "subclasses of BasePasswordHasher must provide %s() method"
483
484    def setUp(self):
485        self.hasher = BasePasswordHasher()
486
487    def test_load_library_no_algorithm(self):
488        msg = "Hasher 'BasePasswordHasher' doesn't specify a library attribute"
489        with self.assertRaisesMessage(ValueError, msg):
490            self.hasher._load_library()
491
492    def test_load_library_importerror(self):
493        PlainHasher = type(
494            "PlainHasher",
495            (BasePasswordHasher,),
496            {"algorithm": "plain", "library": "plain"},
497        )
498        msg = "Couldn't load 'PlainHasher' algorithm library: No module named 'plain'"
499        with self.assertRaisesMessage(ValueError, msg):
500            PlainHasher()._load_library()
501
502    def test_attributes(self):
503        self.assertIsNone(self.hasher.algorithm)
504        self.assertIsNone(self.hasher.library)
505
506    def test_encode(self):
507        msg = self.not_implemented_msg % "an encode"
508        with self.assertRaisesMessage(NotImplementedError, msg):
509            self.hasher.encode("password", "salt")
510
511    def test_harden_runtime(self):
512        msg = (
513            "subclasses of BasePasswordHasher should provide a harden_runtime() method"
514        )
515        with self.assertWarns(Warning, msg=msg):
516            self.hasher.harden_runtime("password", "encoded")
517
518    def test_must_update(self):
519        self.assertIs(self.hasher.must_update("encoded"), False)
520
521    def test_safe_summary(self):
522        msg = self.not_implemented_msg % "a safe_summary"
523        with self.assertRaisesMessage(NotImplementedError, msg):
524            self.hasher.safe_summary("encoded")
525
526    def test_verify(self):
527        msg = self.not_implemented_msg % "a verify"
528        with self.assertRaisesMessage(NotImplementedError, msg):
529            self.hasher.verify("password", "encoded")
530
531
532@skipUnless(argon2, "argon2-cffi not installed")
533@override_settings(PASSWORD_HASHERS=PASSWORD_HASHERS)
534class TestUtilsHashPassArgon2(SimpleTestCase):
535    def test_argon2(self):
536        encoded = make_password("lètmein", hasher="argon2")
537        self.assertTrue(is_password_usable(encoded))
538        self.assertTrue(encoded.startswith("argon2$"))
539        self.assertTrue(check_password("lètmein", encoded))
540        self.assertFalse(check_password("lètmeinz", encoded))
541        self.assertEqual(identify_hasher(encoded).algorithm, "argon2")
542        # Blank passwords
543        blank_encoded = make_password("", hasher="argon2")
544        self.assertTrue(blank_encoded.startswith("argon2$"))
545        self.assertTrue(is_password_usable(blank_encoded))
546        self.assertTrue(check_password("", blank_encoded))
547        self.assertFalse(check_password(" ", blank_encoded))
548        # Old hashes without version attribute
549        encoded = (
550            "argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO"
551            "4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg"
552        )
553        self.assertTrue(check_password("secret", encoded))
554        self.assertFalse(check_password("wrong", encoded))
555
556    def test_argon2_upgrade(self):
557        self._test_argon2_upgrade("time_cost", "time cost", 1)
558        self._test_argon2_upgrade("memory_cost", "memory cost", 16)
559        self._test_argon2_upgrade("parallelism", "parallelism", 1)
560
561    def test_argon2_version_upgrade(self):
562        hasher = get_hasher("argon2")
563        state = {"upgraded": False}
564        encoded = (
565            "argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO"
566            "4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg"
567        )
568
569        def setter(password):
570            state["upgraded"] = True
571
572        old_m = hasher.memory_cost
573        old_t = hasher.time_cost
574        old_p = hasher.parallelism
575        try:
576            hasher.memory_cost = 8
577            hasher.time_cost = 1
578            hasher.parallelism = 1
579            self.assertTrue(check_password("secret", encoded, setter, "argon2"))
580            self.assertTrue(state["upgraded"])
581        finally:
582            hasher.memory_cost = old_m
583            hasher.time_cost = old_t
584            hasher.parallelism = old_p
585
586    def _test_argon2_upgrade(self, attr, summary_key, new_value):
587        hasher = get_hasher("argon2")
588        self.assertEqual("argon2", hasher.algorithm)
589        self.assertNotEqual(getattr(hasher, attr), new_value)
590
591        old_value = getattr(hasher, attr)
592        try:
593            # Generate hash with attr set to 1
594            setattr(hasher, attr, new_value)
595            encoded = make_password("letmein", hasher="argon2")
596            attr_value = hasher.safe_summary(encoded)[summary_key]
597            self.assertEqual(attr_value, new_value)
598
599            state = {"upgraded": False}
600
601            def setter(password):
602                state["upgraded"] = True
603
604            # No upgrade is triggered.
605            self.assertTrue(check_password("letmein", encoded, setter, "argon2"))
606            self.assertFalse(state["upgraded"])
607
608            # Revert to the old rounds count and ...
609            setattr(hasher, attr, old_value)
610
611            # ... check if the password would get updated to the new count.
612            self.assertTrue(check_password("letmein", encoded, setter, "argon2"))
613            self.assertTrue(state["upgraded"])
614        finally:
615            setattr(hasher, attr, old_value)

tests/utils_tests/test_crypto.py

  1import hashlib
  2import unittest
  3
  4from django.utils.crypto import constant_time_compare, pbkdf2
  5
  6
  7class TestUtilsCryptoMisc(unittest.TestCase):
  8    def test_constant_time_compare(self):
  9        # It's hard to test for constant time, just test the result.
 10        self.assertTrue(constant_time_compare(b"spam", b"spam"))
 11        self.assertFalse(constant_time_compare(b"spam", b"eggs"))
 12        self.assertTrue(constant_time_compare("spam", "spam"))
 13        self.assertFalse(constant_time_compare("spam", "eggs"))
 14
 15
 16class TestUtilsCryptoPBKDF2(unittest.TestCase):
 17
 18    # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06
 19    rfc_vectors = [
 20        {
 21            "args": {
 22                "password": "password",
 23                "salt": "salt",
 24                "iterations": 1,
 25                "dklen": 20,
 26                "digest": hashlib.sha1,
 27            },
 28            "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6",
 29        },
 30        {
 31            "args": {
 32                "password": "password",
 33                "salt": "salt",
 34                "iterations": 2,
 35                "dklen": 20,
 36                "digest": hashlib.sha1,
 37            },
 38            "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
 39        },
 40        {
 41            "args": {
 42                "password": "password",
 43                "salt": "salt",
 44                "iterations": 4096,
 45                "dklen": 20,
 46                "digest": hashlib.sha1,
 47            },
 48            "result": "4b007901b765489abead49d926f721d065a429c1",
 49        },
 50        # # this takes way too long :(
 51        # {
 52        #     "args": {
 53        #         "password": "password",
 54        #         "salt": "salt",
 55        #         "iterations": 16777216,
 56        #         "dklen": 20,
 57        #         "digest": hashlib.sha1,
 58        #     },
 59        #     "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",
 60        # },
 61        {
 62            "args": {
 63                "password": "passwordPASSWORDpassword",
 64                "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt",
 65                "iterations": 4096,
 66                "dklen": 25,
 67                "digest": hashlib.sha1,
 68            },
 69            "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
 70        },
 71        {
 72            "args": {
 73                "password": "pass\0word",
 74                "salt": "sa\0lt",
 75                "iterations": 4096,
 76                "dklen": 16,
 77                "digest": hashlib.sha1,
 78            },
 79            "result": "56fa6aa75548099dcc37d7f03425e0c3",
 80        },
 81    ]
 82
 83    regression_vectors = [
 84        {
 85            "args": {
 86                "password": "password",
 87                "salt": "salt",
 88                "iterations": 1,
 89                "dklen": 20,
 90                "digest": hashlib.sha256,
 91            },
 92            "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9",
 93        },
 94        {
 95            "args": {
 96                "password": "password",
 97                "salt": "salt",
 98                "iterations": 1,
 99                "dklen": 20,
100                "digest": hashlib.sha512,
101            },
102            "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6",
103        },
104        {
105            "args": {
106                "password": "password",
107                "salt": "salt",
108                "iterations": 1000,
109                "dklen": 0,
110                "digest": hashlib.sha512,
111            },
112            "result": (
113                "afe6c5530785b6cc6b1c6453384731bd5ee432ee"
114                "549fd42fb6695779ad8a1c5bf59de69c48f774ef"
115                "c4007d5298f9033c0241d5ab69305e7b64eceeb8d"
116                "834cfec"
117            ),
118        },
119        # Check leading zeros are not stripped (#17481)
120        {
121            "args": {
122                "password": b"\xba",
123                "salt": "salt",
124                "iterations": 1,
125                "dklen": 20,
126                "digest": hashlib.sha1,
127            },
128            "result": "0053d3b91a7f1e54effebd6d68771e8a6e0b2c5b",
129        },
130    ]
131
132    def test_public_vectors(self):
133        for vector in self.rfc_vectors:
134            result = pbkdf2(**vector["args"])
135            self.assertEqual(result.hex(), vector["result"])
136
137    def test_regression_vectors(self):
138        for vector in self.regression_vectors:
139            result = pbkdf2(**vector["args"])
140            self.assertEqual(result.hex(), vector["result"])
141
142    def test_default_hmac_alg(self):
143        kwargs = {
144            "password": b"password",
145            "salt": b"salt",
146            "iterations": 1,
147            "dklen": 20,
148        }
149        self.assertEqual(
150            pbkdf2(**kwargs),
151            hashlib.pbkdf2_hmac(hash_name=hashlib.sha256().name, **kwargs),
152        )
153
154
155import hashlib
156import unittest
157
158from django.utils.crypto import constant_time_compare, pbkdf2
159
160
161class TestUtilsCryptoMisc(unittest.TestCase):
162    def test_constant_time_compare(self):
163        # It's hard to test for constant time, just test the result.
164        self.assertTrue(constant_time_compare(b"spam", b"spam"))
165        self.assertFalse(constant_time_compare(b"spam", b"eggs"))
166        self.assertTrue(constant_time_compare("spam", "spam"))
167        self.assertFalse(constant_time_compare("spam", "eggs"))
168
169
170class TestUtilsCryptoPBKDF2(unittest.TestCase):
171
172    # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06
173    rfc_vectors = [
174        {
175            "args": {
176                "password": "password",
177                "salt": "salt",
178                "iterations": 1,
179                "dklen": 20,
180                "digest": hashlib.sha1,
181            },
182            "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6",
183        },
184        {
185            "args": {
186                "password": "password",
187                "salt": "salt",
188                "iterations": 2,
189                "dklen": 20,
190                "digest": hashlib.sha1,
191            },
192            "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
193        },
194        {
195            "args": {
196                "password": "password",
197                "salt": "salt",
198                "iterations": 4096,
199                "dklen": 20,
200                "digest": hashlib.sha1,
201            },
202            "result": "4b007901b765489abead49d926f721d065a429c1",
203        },
204        # # this takes way too long :(
205        # {
206        #     "args": {
207        #         "password": "password",
208        #         "salt": "salt",
209        #         "iterations": 16777216,
210        #         "dklen": 20,
211        #         "digest": hashlib.sha1,
212        #     },
213        #     "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984",
214        # },
215        {
216            "args": {
217                "password": "passwordPASSWORDpassword",
218                "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt",
219                "iterations": 4096,
220                "dklen": 25,
221                "digest": hashlib.sha1,
222            },
223            "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
224        },
225        {
226            "args": {
227                "password": "pass\0word",
228                "salt": "sa\0lt",
229                "iterations": 4096,
230                "dklen": 16,
231                "digest": hashlib.sha1,
232            },
233            "result": "56fa6aa75548099dcc37d7f03425e0c3",
234        },
235    ]
236
237    regression_vectors = [
238        {
239            "args": {
240                "password": "password",
241                "salt": "salt",
242                "iterations": 1,
243                "dklen": 20,
244                "digest": hashlib.sha256,
245            },
246            "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9",
247        },
248        {
249            "args": {
250                "password": "password",
251                "salt": "salt",
252                "iterations": 1,
253                "dklen": 20,
254                "digest": hashlib.sha512,
255            },
256            "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6",
257        },
258        {
259            "args": {
260                "password": "password",
261                "salt": "salt",
262                "iterations": 1000,
263                "dklen": 0,
264                "digest": hashlib.sha512,
265            },
266            "result": (
267                "afe6c5530785b6cc6b1c6453384731bd5ee432ee"
268                "549fd42fb6695779ad8a1c5bf59de69c48f774ef"
269                "c4007d5298f9033c0241d5ab69305e7b64eceeb8d"
270                "834cfec"
271            ),
272        },
273        # Check leading zeros are not stripped (#17481)
274        {
275            "args": {
276                "password": b"\xba",
277                "salt": "salt",
278                "iterations": 1,
279                "dklen": 20,
280                "digest": hashlib.sha1,
281            },
282            "result": "0053d3b91a7f1e54effebd6d68771e8a6e0b2c5b",
283        },
284    ]
285
286    def test_public_vectors(self):
287        for vector in self.rfc_vectors:
288            result = pbkdf2(**vector["args"])
289            self.assertEqual(result.hex(), vector["result"])
290
291    def test_regression_vectors(self):
292        for vector in self.regression_vectors:
293            result = pbkdf2(**vector["args"])
294            self.assertEqual(result.hex(), vector["result"])
295
296    def test_default_hmac_alg(self):
297        kwargs = {
298            "password": b"password",
299            "salt": b"salt",
300            "iterations": 1,
301            "dklen": 20,
302        }
303        self.assertEqual(
304            pbkdf2(**kwargs),
305            hashlib.pbkdf2_hmac(hash_name=hashlib.sha256().name, **kwargs),
306        )