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