This commit is contained in:
Oguntunde Caleb 2023-05-12 02:07:14 +01:00
parent db2c45c729
commit 05b389f668
3 changed files with 162 additions and 167 deletions

View File

@ -41,6 +41,7 @@ The `api_settings` object will check for any user-defined settings, and otherwis
*The following settings control the basic API policies, and are applied to every `APIView` class-based view, or `@api_view` function based view.*
<<<<<<< HEAD
#### DEFAULT_MIDDLEWARE_CLASSES
A list or tuple of middleware classes, that is run prior to calling the method handler.
@ -48,6 +49,8 @@ A list or tuple of middleware classes, that is run prior to calling the method h
Default: `[]`
=======
>>>>>>> parent of 700cbf19 (Add middleware classes support that run after drf mutate request)
#### DEFAULT_RENDERER_CLASSES
A list or tuple of renderer classes, that determines the default set of renderers that may be used when returning a `Response` object.

View File

@ -19,7 +19,6 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from django.conf import settings
# Import from `django.core.signals` instead of the official location
# `django.test.signals` to avoid importing the test module unnecessarily.
from django.core.signals import setting_changed
@ -28,118 +27,138 @@ from django.utils.module_loading import import_string
from rest_framework import ISO_8601
DEFAULTS = {
<<<<<<< HEAD
# custom middleware class to run prior to calling the method handler
"DEFAULT_MIDDLEWARE_CLASSES": [],
=======
>>>>>>> parent of 700cbf19 (Add middleware classes support that run after drf mutate request)
# Base API policies
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.FormParser",
"rest_framework.parsers.MultiPartParser",
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
"DEFAULT_THROTTLE_CLASSES": [],
"DEFAULT_CONTENT_NEGOTIATION_CLASS": "rest_framework.negotiation.DefaultContentNegotiation",
"DEFAULT_METADATA_CLASS": "rest_framework.metadata.SimpleMetadata",
"DEFAULT_VERSIONING_CLASS": None,
'DEFAULT_THROTTLE_CLASSES': [],
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
'DEFAULT_VERSIONING_CLASS': None,
# Generic view behavior
"DEFAULT_PAGINATION_CLASS": None,
"DEFAULT_FILTER_BACKENDS": [],
'DEFAULT_PAGINATION_CLASS': None,
'DEFAULT_FILTER_BACKENDS': [],
# Schema
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.openapi.AutoSchema",
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.openapi.AutoSchema',
# Throttling
"DEFAULT_THROTTLE_RATES": {
"user": None,
"anon": None,
'DEFAULT_THROTTLE_RATES': {
'user': None,
'anon': None,
},
"NUM_PROXIES": None,
'NUM_PROXIES': None,
# Pagination
"PAGE_SIZE": None,
'PAGE_SIZE': None,
# Filtering
"SEARCH_PARAM": "search",
"ORDERING_PARAM": "ordering",
'SEARCH_PARAM': 'search',
'ORDERING_PARAM': 'ordering',
# Versioning
"DEFAULT_VERSION": None,
"ALLOWED_VERSIONS": None,
"VERSION_PARAM": "version",
'DEFAULT_VERSION': None,
'ALLOWED_VERSIONS': None,
'VERSION_PARAM': 'version',
# Authentication
"UNAUTHENTICATED_USER": "django.contrib.auth.models.AnonymousUser",
"UNAUTHENTICATED_TOKEN": None,
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
# View configuration
"VIEW_NAME_FUNCTION": "rest_framework.views.get_view_name",
"VIEW_DESCRIPTION_FUNCTION": "rest_framework.views.get_view_description",
'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
# Exception handling
"EXCEPTION_HANDLER": "rest_framework.views.exception_handler",
"NON_FIELD_ERRORS_KEY": "non_field_errors",
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
'NON_FIELD_ERRORS_KEY': 'non_field_errors',
# Testing
"TEST_REQUEST_RENDERER_CLASSES": [
"rest_framework.renderers.MultiPartRenderer",
"rest_framework.renderers.JSONRenderer",
'TEST_REQUEST_RENDERER_CLASSES': [
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer'
],
"TEST_REQUEST_DEFAULT_FORMAT": "multipart",
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
# Hyperlink settings
"URL_FORMAT_OVERRIDE": "format",
"FORMAT_SUFFIX_KWARG": "format",
"URL_FIELD_NAME": "url",
'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format',
'URL_FIELD_NAME': 'url',
# Input and output formats
"DATE_FORMAT": ISO_8601,
"DATE_INPUT_FORMATS": [ISO_8601],
"DATETIME_FORMAT": ISO_8601,
"DATETIME_INPUT_FORMATS": [ISO_8601],
"TIME_FORMAT": ISO_8601,
"TIME_INPUT_FORMATS": [ISO_8601],
'DATE_FORMAT': ISO_8601,
'DATE_INPUT_FORMATS': [ISO_8601],
'DATETIME_FORMAT': ISO_8601,
'DATETIME_INPUT_FORMATS': [ISO_8601],
'TIME_FORMAT': ISO_8601,
'TIME_INPUT_FORMATS': [ISO_8601],
# Encoding
"UNICODE_JSON": True,
"COMPACT_JSON": True,
"STRICT_JSON": True,
"COERCE_DECIMAL_TO_STRING": True,
"UPLOADED_FILES_USE_URL": True,
'UNICODE_JSON': True,
'COMPACT_JSON': True,
'STRICT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True,
# Browseable API
"HTML_SELECT_CUTOFF": 1000,
"HTML_SELECT_CUTOFF_TEXT": "More than {count} items...",
'HTML_SELECT_CUTOFF': 1000,
'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...",
# Schemas
"SCHEMA_COERCE_PATH_PK": True,
"SCHEMA_COERCE_METHOD_NAMES": {"retrieve": "read", "destroy": "delete"},
'SCHEMA_COERCE_PATH_PK': True,
'SCHEMA_COERCE_METHOD_NAMES': {
'retrieve': 'read',
'destroy': 'delete'
},
}
# List of settings that may be in string import notation.
IMPORT_STRINGS = [
"MIDDLEWARE_CLASSES",
"DEFAULT_RENDERER_CLASSES",
"DEFAULT_PARSER_CLASSES",
"DEFAULT_AUTHENTICATION_CLASSES",
"DEFAULT_PERMISSION_CLASSES",
"DEFAULT_THROTTLE_CLASSES",
"DEFAULT_CONTENT_NEGOTIATION_CLASS",
"DEFAULT_METADATA_CLASS",
"DEFAULT_VERSIONING_CLASS",
"DEFAULT_PAGINATION_CLASS",
"DEFAULT_FILTER_BACKENDS",
"DEFAULT_SCHEMA_CLASS",
"EXCEPTION_HANDLER",
"TEST_REQUEST_RENDERER_CLASSES",
"UNAUTHENTICATED_USER",
"UNAUTHENTICATED_TOKEN",
"VIEW_NAME_FUNCTION",
"VIEW_DESCRIPTION_FUNCTION",
'DEFAULT_RENDERER_CLASSES',
'DEFAULT_PARSER_CLASSES',
'DEFAULT_AUTHENTICATION_CLASSES',
'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_METADATA_CLASS',
'DEFAULT_VERSIONING_CLASS',
'DEFAULT_PAGINATION_CLASS',
'DEFAULT_FILTER_BACKENDS',
'DEFAULT_SCHEMA_CLASS',
'EXCEPTION_HANDLER',
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
'VIEW_NAME_FUNCTION',
'VIEW_DESCRIPTION_FUNCTION'
]
# List of settings that have been removed
REMOVED_SETTINGS = [
"PAGINATE_BY",
"PAGINATE_BY_PARAM",
"MAX_PAGINATE_BY",
'PAGINATE_BY', 'PAGINATE_BY_PARAM', 'MAX_PAGINATE_BY',
]
@ -164,12 +183,7 @@ def import_from_string(val, setting_name):
try:
return import_string(val)
except ImportError as e:
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (
val,
setting_name,
e.__class__.__name__,
e,
)
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
@ -189,7 +203,6 @@ class APISettings:
under the REST_FRAMEWORK name. It is not intended to be used by 3rd-party
apps, and test helpers like `override_settings` may not work as expected.
"""
def __init__(self, user_settings=None, defaults=None, import_strings=None):
if user_settings:
self._user_settings = self.__check_user_settings(user_settings)
@ -199,8 +212,8 @@ class APISettings:
@property
def user_settings(self):
if not hasattr(self, "_user_settings"):
self._user_settings = getattr(settings, "REST_FRAMEWORK", {})
if not hasattr(self, '_user_settings'):
self._user_settings = getattr(settings, 'REST_FRAMEWORK', {})
return self._user_settings
def __getattr__(self, attr):
@ -227,26 +240,23 @@ class APISettings:
SETTINGS_DOC = "https://www.django-rest-framework.org/api-guide/settings/"
for setting in REMOVED_SETTINGS:
if setting in user_settings:
raise RuntimeError(
"The '%s' setting has been removed. Please refer to '%s' for available settings."
% (setting, SETTINGS_DOC)
)
raise RuntimeError("The '%s' setting has been removed. Please refer to '%s' for available settings." % (setting, SETTINGS_DOC))
return user_settings
def reload(self):
for attr in self._cached_attrs:
delattr(self, attr)
self._cached_attrs.clear()
if hasattr(self, "_user_settings"):
delattr(self, "_user_settings")
if hasattr(self, '_user_settings'):
delattr(self, '_user_settings')
api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)
def reload_api_settings(*args, **kwargs):
setting = kwargs["setting"]
if setting == "REST_FRAMEWORK":
setting = kwargs['setting']
if setting == 'REST_FRAMEWORK':
api_settings.reload()

View File

@ -27,19 +27,19 @@ def get_view_name(view):
This function is the default for the `VIEW_NAME_FUNCTION` setting.
"""
# Name may be set by some Views, such as a ViewSet.
name = getattr(view, "name", None)
name = getattr(view, 'name', None)
if name is not None:
return name
name = view.__class__.__name__
name = formatting.remove_trailing_string(name, "View")
name = formatting.remove_trailing_string(name, "ViewSet")
name = formatting.remove_trailing_string(name, 'View')
name = formatting.remove_trailing_string(name, 'ViewSet')
name = formatting.camelcase_to_spaces(name)
# Suffix may be set by some Views, such as a ViewSet.
suffix = getattr(view, "suffix", None)
suffix = getattr(view, 'suffix', None)
if suffix:
name += " " + suffix
name += ' ' + suffix
return name
@ -52,9 +52,9 @@ def get_view_description(view, html=False):
This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
"""
# Description may be set by some Views, such as a ViewSet.
description = getattr(view, "description", None)
description = getattr(view, 'description', None)
if description is None:
description = view.__class__.__doc__ or ""
description = view.__class__.__doc__ or ''
description = formatting.dedent(smart_str(description))
if html:
@ -64,7 +64,7 @@ def get_view_description(view, html=False):
def set_rollback():
for db in connections.all():
if db.settings_dict["ATOMIC_REQUESTS"] and db.in_atomic_block:
if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block:
db.set_rollback(True)
@ -85,15 +85,15 @@ def exception_handler(exc, context):
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["Retry-After"] = "%d" % exc.wait
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {"detail": exc.detail}
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
@ -102,8 +102,12 @@ def exception_handler(exc, context):
class APIView(View):
# The following policies may be set at either globally, or per-view.
<<<<<<< HEAD
middleware_classes = api_settings.DEFAULT_MIDDLEWARE_CLASSES
=======
>>>>>>> parent of 700cbf19 (Add middleware classes support that run after drf mutate request)
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
@ -126,15 +130,13 @@ class APIView(View):
This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
if isinstance(getattr(cls, "queryset", None), models.query.QuerySet):
if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
def force_evaluation():
raise RuntimeError(
"Do not evaluate the `.queryset` attribute directly, "
"as the result will be cached and reused between requests. "
"Use `.all()` or call `.get_queryset()` instead."
'Do not evaluate the `.queryset` attribute directly, '
'as the result will be cached and reused between requests. '
'Use `.all()` or call `.get_queryset()` instead.'
)
cls.queryset._fetch_all = force_evaluation
view = super().as_view(**initkwargs)
@ -155,10 +157,10 @@ class APIView(View):
@property
def default_response_headers(self):
headers = {
"Allow": ", ".join(self.allowed_methods),
'Allow': ', '.join(self.allowed_methods),
}
if len(self.renderer_classes) > 1:
headers["Vary"] = "Accept"
headers['Vary'] = 'Accept'
return headers
def http_method_not_allowed(self, request, *args, **kwargs):
@ -199,9 +201,9 @@ class APIView(View):
# Note: Additionally `request` and `encoding` will also be added
# to the context by the Request object.
return {
"view": self,
"args": getattr(self, "args", ()),
"kwargs": getattr(self, "kwargs", {}),
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {})
}
def get_renderer_context(self):
@ -212,10 +214,10 @@ class APIView(View):
# Note: Additionally 'response' will also be added to the context,
# by the Response object.
return {
"view": self,
"args": getattr(self, "args", ()),
"kwargs": getattr(self, "kwargs", {}),
"request": getattr(self, "request", None),
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None)
}
def get_exception_handler_context(self):
@ -224,10 +226,10 @@ class APIView(View):
as the `context` argument.
"""
return {
"view": self,
"args": getattr(self, "args", ()),
"kwargs": getattr(self, "kwargs", {}),
"request": getattr(self, "request", None),
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None)
}
def get_view_name(self):
@ -289,7 +291,7 @@ class APIView(View):
"""
Instantiate and return the content negotiation class to use.
"""
if not getattr(self, "_negotiator", None):
if not getattr(self, '_negotiator', None):
self._negotiator = self.content_negotiation_class()
return self._negotiator
@ -334,8 +336,8 @@ class APIView(View):
if not permission.has_permission(request, self):
self.permission_denied(
request,
message=getattr(permission, "message", None),
code=getattr(permission, "code", None),
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
def check_object_permissions(self, request, obj):
@ -347,8 +349,8 @@ class APIView(View):
if not permission.has_object_permission(request, self, obj):
self.permission_denied(
request,
message=getattr(permission, "message", None),
code=getattr(permission, "code", None),
message=getattr(permission, 'message', None),
code=getattr(permission, 'code', None)
)
def check_throttles(self, request):
@ -365,7 +367,8 @@ class APIView(View):
# Filter out `None` values which may happen in case of config / rate
# changes, see #1438
durations = [
duration for duration in throttle_durations if duration is not None
duration for duration in throttle_durations
if duration is not None
]
duration = max(durations, default=None)
@ -383,23 +386,6 @@ class APIView(View):
# Dispatch methods
def get_middleware_classes(self):
"""
get list of middleware class instance
"""
return [middleware() for middleware in self.middleware_classes]
def initialize_middleware_classes(self, request):
"""
Run custom middleware classes before prior to calling the method handler
"""
for middleware in self.get_middleware_classes():
middleware(request)
return request # Return mutated request
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
@ -411,7 +397,7 @@ class APIView(View):
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context,
parser_context=parser_context
)
def initial(self, request, *args, **kwargs):
@ -433,21 +419,19 @@ class APIView(View):
self.check_permissions(request)
self.check_throttles(request)
# authentication and other task ran before final mutation
self.initialize_middleware_classes(request)
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
"""
# Make the error obvious if a proper response is not returned
assert isinstance(response, HttpResponseBase), (
"Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` "
"to be returned from the view, but received a `%s`" % type(response)
'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
'to be returned from the view, but received a `%s`'
% type(response)
)
if isinstance(response, Response):
if not getattr(request, "accepted_renderer", None):
if not getattr(request, 'accepted_renderer', None):
neg = self.perform_content_negotiation(request, force=True)
request.accepted_renderer, request.accepted_media_type = neg
@ -456,7 +440,7 @@ class APIView(View):
response.renderer_context = self.get_renderer_context()
# Add new vary headers to the response instead of overwriting.
vary_headers = self.headers.pop("Vary", None)
vary_headers = self.headers.pop('Vary', None)
if vary_headers is not None:
patch_vary_headers(response, cc_delim_re.split(vary_headers))
@ -470,9 +454,8 @@ class APIView(View):
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
if isinstance(
exc, (exceptions.NotAuthenticated, exceptions.AuthenticationFailed)
):
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request)
@ -495,8 +478,8 @@ class APIView(View):
def raise_uncaught_exception(self, exc):
if settings.DEBUG:
request = self.request
renderer_format = getattr(request.accepted_renderer, "format")
use_plaintext_traceback = renderer_format not in ("html", "api", "admin")
renderer_format = getattr(request.accepted_renderer, 'format')
use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin')
request.force_plaintext_errors(use_plaintext_traceback)
raise exc
@ -519,9 +502,8 @@ class APIView(View):
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(
self, request.method.lower(), self.http_method_not_allowed
)
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed