django.contrib.auth (django/contrib/auth/backends.py) ¶
See also
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 += "?" + "&".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 ¶
See also
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 ¶
See also
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 )