diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index 92771c56df..4b8b8804b0 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -1,5 +1,7 @@ from functools import wraps +from asgiref.sync import iscoroutinefunction + from django.middleware.csrf import CsrfViewMiddleware, get_token from django.utils.decorators import decorator_from_middleware @@ -51,9 +53,17 @@ def csrf_exempt(view_func): # view_func.csrf_exempt = True would also work, but decorators are nicer # if they don't have side effects, so return a new function. - @wraps(view_func) - def wrapper_view(*args, **kwargs): - return view_func(*args, **kwargs) - wrapper_view.csrf_exempt = True - return wrapper_view + if iscoroutinefunction(view_func): + + async def _view_wrapper(request, *args, **kwargs): + return await view_func(request, *args, **kwargs) + + else: + + def _view_wrapper(request, *args, **kwargs): + return view_func(request, *args, **kwargs) + + _view_wrapper.csrf_exempt = True + + return wraps(view_func)(_view_wrapper) diff --git a/docs/ref/csrf.txt b/docs/ref/csrf.txt index 583f78472c..d52539e6b6 100644 --- a/docs/ref/csrf.txt +++ b/docs/ref/csrf.txt @@ -150,6 +150,10 @@ class-based views`. def my_view(request): return HttpResponse("Hello world") + .. versionchanged:: 5.0 + + Support for wrapping asynchronous view functions was added. + .. function:: csrf_protect(view) Decorator that provides the protection of ``CsrfViewMiddleware`` to a view. diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index 722d3bcf0b..8884886b51 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -258,6 +258,7 @@ Decorators * :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.never_cache` * :func:`~django.views.decorators.common.no_append_slash` + * :func:`~django.views.decorators.csrf.csrf_exempt` * :func:`~django.views.decorators.debug.sensitive_variables` * :func:`~django.views.decorators.debug.sensitive_post_parameters` * :func:`~django.views.decorators.http.condition` diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 6713914283..fb50f007e2 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -84,6 +84,7 @@ view functions: * :func:`~django.views.decorators.cache.cache_control` * :func:`~django.views.decorators.cache.never_cache` * :func:`~django.views.decorators.common.no_append_slash` +* :func:`~django.views.decorators.csrf.csrf_exempt` * :func:`~django.views.decorators.http.condition` * :func:`~django.views.decorators.http.etag` * :func:`~django.views.decorators.http.last_modified` diff --git a/tests/decorators/test_csrf.py b/tests/decorators/test_csrf.py new file mode 100644 index 0000000000..275b12ee9d --- /dev/null +++ b/tests/decorators/test_csrf.py @@ -0,0 +1,37 @@ +from asgiref.sync import iscoroutinefunction + +from django.http import HttpRequest, HttpResponse +from django.test import SimpleTestCase +from django.views.decorators.csrf import csrf_exempt + + +class CsrfExemptTests(SimpleTestCase): + def test_wrapped_sync_function_is_not_coroutine_function(self): + def sync_view(request): + return HttpResponse() + + wrapped_view = csrf_exempt(sync_view) + self.assertIs(iscoroutinefunction(wrapped_view), False) + + def test_wrapped_async_function_is_coroutine_function(self): + async def async_view(request): + return HttpResponse() + + wrapped_view = csrf_exempt(async_view) + self.assertIs(iscoroutinefunction(wrapped_view), True) + + def test_csrf_exempt_decorator(self): + @csrf_exempt + def sync_view(request): + return HttpResponse() + + self.assertIs(sync_view.csrf_exempt, True) + self.assertIsInstance(sync_view(HttpRequest()), HttpResponse) + + async def test_csrf_exempt_decorator_async_view(self): + @csrf_exempt + async def async_view(request): + return HttpResponse() + + self.assertIs(async_view.csrf_exempt, True) + self.assertIsInstance(await async_view(HttpRequest()), HttpResponse)