base.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import logging
  2. from asgiref.sync import iscoroutinefunction, markcoroutinefunction
  3. from django.core.exceptions import ImproperlyConfigured
  4. from django.http import (
  5. HttpResponse,
  6. HttpResponseGone,
  7. HttpResponseNotAllowed,
  8. HttpResponsePermanentRedirect,
  9. HttpResponseRedirect,
  10. )
  11. from django.template.response import TemplateResponse
  12. from django.urls import reverse
  13. from django.utils.decorators import classonlymethod
  14. from django.utils.functional import classproperty
  15. from django.utils.log import log_response
  16. logger = logging.getLogger("django.request")
  17. class ContextMixin:
  18. """
  19. A default context mixin that passes the keyword arguments received by
  20. get_context_data() as the template context.
  21. """
  22. extra_context = None
  23. def get_context_data(self, **kwargs):
  24. kwargs.setdefault("view", self)
  25. if self.extra_context is not None:
  26. kwargs.update(self.extra_context)
  27. return kwargs
  28. class View:
  29. """
  30. Intentionally simple parent class for all views. Only implements
  31. dispatch-by-method and simple sanity checking.
  32. """
  33. http_method_names = [
  34. "get",
  35. "post",
  36. "put",
  37. "patch",
  38. "delete",
  39. "head",
  40. "options",
  41. "trace",
  42. ]
  43. def __init__(self, **kwargs):
  44. """
  45. Constructor. Called in the URLconf; can contain helpful extra
  46. keyword arguments, and other things.
  47. """
  48. # Go through keyword arguments, and either save their values to our
  49. # instance, or raise an error.
  50. for key, value in kwargs.items():
  51. setattr(self, key, value)
  52. @classproperty
  53. def view_is_async(cls):
  54. handlers = [
  55. getattr(cls, method)
  56. for method in cls.http_method_names
  57. if (method != "options" and hasattr(cls, method))
  58. ]
  59. if not handlers:
  60. return False
  61. is_async = iscoroutinefunction(handlers[0])
  62. if not all(iscoroutinefunction(h) == is_async for h in handlers[1:]):
  63. raise ImproperlyConfigured(
  64. f"{cls.__qualname__} HTTP handlers must either be all sync or all "
  65. "async."
  66. )
  67. return is_async
  68. @classonlymethod
  69. def as_view(cls, **initkwargs):
  70. """Main entry point for a request-response process."""
  71. for key in initkwargs:
  72. if key in cls.http_method_names:
  73. raise TypeError(
  74. "The method name %s is not accepted as a keyword argument "
  75. "to %s()." % (key, cls.__name__)
  76. )
  77. if not hasattr(cls, key):
  78. raise TypeError(
  79. "%s() received an invalid keyword %r. as_view "
  80. "only accepts arguments that are already "
  81. "attributes of the class." % (cls.__name__, key)
  82. )
  83. def view(request, *args, **kwargs):
  84. self = cls(**initkwargs)
  85. self.setup(request, *args, **kwargs)
  86. if not hasattr(self, "request"):
  87. raise AttributeError(
  88. "%s instance has no 'request' attribute. Did you override "
  89. "setup() and forget to call super()?" % cls.__name__
  90. )
  91. return self.dispatch(request, *args, **kwargs)
  92. view.view_class = cls
  93. view.view_initkwargs = initkwargs
  94. # __name__ and __qualname__ are intentionally left unchanged as
  95. # view_class should be used to robustly determine the name of the view
  96. # instead.
  97. view.__doc__ = cls.__doc__
  98. view.__module__ = cls.__module__
  99. view.__annotations__ = cls.dispatch.__annotations__
  100. # Copy possible attributes set by decorators, e.g. @csrf_exempt, from
  101. # the dispatch method.
  102. view.__dict__.update(cls.dispatch.__dict__)
  103. # Mark the callback if the view class is async.
  104. if cls.view_is_async:
  105. markcoroutinefunction(view)
  106. return view
  107. def setup(self, request, *args, **kwargs):
  108. """Initialize attributes shared by all view methods."""
  109. if hasattr(self, "get") and not hasattr(self, "head"):
  110. self.head = self.get
  111. self.request = request
  112. self.args = args
  113. self.kwargs = kwargs
  114. def dispatch(self, request, *args, **kwargs):
  115. # Try to dispatch to the right method; if a method doesn't exist,
  116. # defer to the error handler. Also defer to the error handler if the
  117. # request method isn't on the approved list.
  118. if request.method.lower() in self.http_method_names:
  119. handler = getattr(
  120. self, request.method.lower(), self.http_method_not_allowed
  121. )
  122. else:
  123. handler = self.http_method_not_allowed
  124. return handler(request, *args, **kwargs)
  125. def http_method_not_allowed(self, request, *args, **kwargs):
  126. response = HttpResponseNotAllowed(self._allowed_methods())
  127. log_response(
  128. "Method Not Allowed (%s): %s",
  129. request.method,
  130. request.path,
  131. response=response,
  132. request=request,
  133. )
  134. if self.view_is_async:
  135. async def func():
  136. return response
  137. return func()
  138. else:
  139. return response
  140. def options(self, request, *args, **kwargs):
  141. """Handle responding to requests for the OPTIONS HTTP verb."""
  142. response = HttpResponse()
  143. response.headers["Allow"] = ", ".join(self._allowed_methods())
  144. response.headers["Content-Length"] = "0"
  145. if self.view_is_async:
  146. async def func():
  147. return response
  148. return func()
  149. else:
  150. return response
  151. def _allowed_methods(self):
  152. return [m.upper() for m in self.http_method_names if hasattr(self, m)]
  153. class TemplateResponseMixin:
  154. """A mixin that can be used to render a template."""
  155. template_name = None
  156. template_engine = None
  157. response_class = TemplateResponse
  158. content_type = None
  159. def render_to_response(self, context, **response_kwargs):
  160. """
  161. Return a response, using the `response_class` for this view, with a
  162. template rendered with the given context.
  163. Pass response_kwargs to the constructor of the response class.
  164. """
  165. response_kwargs.setdefault("content_type", self.content_type)
  166. return self.response_class(
  167. request=self.request,
  168. template=self.get_template_names(),
  169. context=context,
  170. using=self.template_engine,
  171. **response_kwargs,
  172. )
  173. def get_template_names(self):
  174. """
  175. Return a list of template names to be used for the request. Must return
  176. a list. May not be called if render_to_response() is overridden.
  177. """
  178. if self.template_name is None:
  179. raise ImproperlyConfigured(
  180. "TemplateResponseMixin requires either a definition of "
  181. "'template_name' or an implementation of 'get_template_names()'"
  182. )
  183. else:
  184. return [self.template_name]
  185. class TemplateView(TemplateResponseMixin, ContextMixin, View):
  186. """
  187. Render a template. Pass keyword arguments from the URLconf to the context.
  188. """
  189. def get(self, request, *args, **kwargs):
  190. context = self.get_context_data(**kwargs)
  191. return self.render_to_response(context)
  192. class RedirectView(View):
  193. """Provide a redirect on any GET request."""
  194. permanent = False
  195. url = None
  196. pattern_name = None
  197. query_string = False
  198. def get_redirect_url(self, *args, **kwargs):
  199. """
  200. Return the URL redirect to. Keyword arguments from the URL pattern
  201. match generating the redirect request are provided as kwargs to this
  202. method.
  203. """
  204. if self.url:
  205. url = self.url % kwargs
  206. elif self.pattern_name:
  207. url = reverse(self.pattern_name, args=args, kwargs=kwargs)
  208. else:
  209. return None
  210. args = self.request.META.get("QUERY_STRING", "")
  211. if args and self.query_string:
  212. url = "%s?%s" % (url, args)
  213. return url
  214. def get(self, request, *args, **kwargs):
  215. url = self.get_redirect_url(*args, **kwargs)
  216. if url:
  217. if self.permanent:
  218. return HttpResponsePermanentRedirect(url)
  219. else:
  220. return HttpResponseRedirect(url)
  221. else:
  222. response = HttpResponseGone()
  223. log_response("Gone: %s", request.path, response=response, request=request)
  224. return response
  225. def head(self, request, *args, **kwargs):
  226. return self.get(request, *args, **kwargs)
  227. def post(self, request, *args, **kwargs):
  228. return self.get(request, *args, **kwargs)
  229. def options(self, request, *args, **kwargs):
  230. return self.get(request, *args, **kwargs)
  231. def delete(self, request, *args, **kwargs):
  232. return self.get(request, *args, **kwargs)
  233. def put(self, request, *args, **kwargs):
  234. return self.get(request, *args, **kwargs)
  235. def patch(self, request, *args, **kwargs):
  236. return self.get(request, *args, **kwargs)