Views
Take a request, return a response.
Plain views are written as classes, with a straightforward API that keeps simple views simple, but gives you the power of a full class to handle more complex cases.
from plain.views import View
class ExampleView(View):
def get(self):
return "Hello, world!"
HTTP methods -> class methods
The HTTP methd of the request will map to a class method of the same name on the view.
If a request comes in and there isn't a matching method on the view,
Plain will return a 405 Method Not Allowed
response.
from plain.views import View
class ExampleView(View):
def get(self):
pass
def post(self):
pass
def put(self):
pass
def patch(self):
pass
def delete(self):
pass
def trace(self):
pass
The base View
class defines default options
and head
behavior,
but you can override these too.
Return types
For simple plain text and JSON responses,
you don't need to instantiate a Response
object.
class TextView(View):
def get(self):
return "Hello, world!"
class JsonView(View):
def get(self):
return {"message": "Hello, world!"}
Template views
The most common behavior for a view is to render a template.
from plain.views import TemplateView
class ExampleView(TemplateView):
template_name = "example.html"
def get_template_context(self):
context = super().get_template_context()
context["message"] = "Hello, world!"
return context
The TemplateView
is also the base class for most of the other built-in view classes.
Form views
Standard forms can be rendered and processed by a FormView
.
from plain.views import FormView
from .forms import ExampleForm
class ExampleView(FormView):
template_name = "example.html"
form_class = ExampleForm
success_url = "." # Redirect to the same page
def form_valid(self, form):
# Do other successfull form processing here
return super().form_valid(form)
Rendering forms is done directly in the HTML.
{% extends "base.html" %}
{% block content %}
<form method="post">
{{ csrf_input }}
<!-- Render general form errors -->
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
<!-- Render form fields individually (or with Jinja helps or other concepts) -->
<label for="{{ form.email.html_id }}">Email</label>
<input
type="email"
name="{{ form.email.html_name }}"
id="{{ form.email.html_id }}"
value="{{ form.email.value() or '' }}"
autocomplete="email"
autofocus
required>
{% if form.email.errors %}
<div>{{ form.email.errors|join(', ') }}</div>
{% endif %}
<button type="submit">Save</button>
</form>
{% endblock %}
Object views
The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.
from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView
class ExampleDetailView(DetailView):
template_name = "detail.html"
def get_object(self):
return MyObjectClass.objects.get(
pk=self.url_kwargs["pk"],
user=self.request.user, # Limit access
)
class ExampleCreateView(CreateView):
template_name = "create.html"
form_class = CustomCreateForm
success_url = "."
class ExampleUpdateView(UpdateView):
template_name = "update.html"
form_class = CustomUpdateForm
success_url = "."
def get_object(self):
return MyObjectClass.objects.get(
pk=self.url_kwargs["pk"],
user=self.request.user, # Limit access
)
class ExampleDeleteView(DeleteView):
template_name = "delete.html"
success_url = "."
# No form class necessary.
# Just POST to this view to delete the object.
def get_object(self):
return MyObjectClass.objects.get(
pk=self.url_kwargs["pk"],
user=self.request.user, # Limit access
)
class ExampleListView(ListView):
template_name = "list.html"
def get_objects(self):
return MyObjectClass.objects.filter(
user=self.request.user, # Limit access
)
Response exceptions
At any point in the request handling,
a view can raise a ResponseException
to immediately exit and return the wrapped response.
This isn't always necessary, but can be useful for raising rate limits or authorization errors when you're a couple layers deep in the view handling or helper functions.
from plain.views import DetailView
from plain.views.exceptions import ResponseException
from plain.http import Response
class ExampleView(DetailView):
def get_object(self):
if self.request.user.exceeds_rate_limit:
raise ResponseException(
Response("Rate limit exceeded", status=429)
)
return AnExpensiveObject()
Error views
By default, HTTP errors will be rendered by templates/<status_code>.html
or templates/error.html
.
You can define your own error views by pointing the HTTP_ERROR_VIEWS
setting to a dictionary of status codes and view classes.
# app/settings.py
HTTP_ERROR_VIEWS = {
404: "errors.NotFoundView",
}
# app/errors.py
from plain.views import View
class NotFoundView(View):
def get(self):
# A custom implementation or error view handling
pass
Redirect views
from plain.views import RedirectView
class ExampleRedirectView(RedirectView):
url = "/new-location/"
permanent = True
CSRF exemption
from plain.views import View
from plain.views.csrf import CsrfExemptViewMixin
class ExemptView(CsrfExemptViewMixin, View):
def post(self):
return "Hello, world!"
1from plain.exceptions import ImproperlyConfigured, ObjectDoesNotExist
2from plain.forms import Form
3from plain.http import Http404, Response
4
5from .forms import FormView
6from .templates import TemplateView
7
8
9class ObjectTemplateViewMixin:
10 context_object_name = ""
11
12 def get(self) -> Response:
13 self.load_object()
14 return self.render_template()
15
16 def load_object(self) -> None:
17 try:
18 self.object = self.get_object()
19 except ObjectDoesNotExist:
20 raise Http404
21
22 if not self.object:
23 # Also raise 404 if the object is None
24 raise Http404
25
26 def get_object(self): # Intentionally untyped... subclasses must override this.
27 raise NotImplementedError(
28 f"get_object() is not implemented on {self.__class__.__name__}"
29 )
30
31 def get_template_context(self) -> dict:
32 """Insert the single object into the context dict."""
33 context = super().get_template_context() # type: ignore
34 context["object"] = self.object
35 if self.context_object_name:
36 context[self.context_object_name] = self.object
37 elif hasattr(self.object, "_meta"):
38 context[self.object._meta.model_name] = self.object
39 return context
40
41 def get_template_names(self) -> list[str]:
42 """
43 Return a list of template names to be used for the request. May not be
44 called if render_to_response() is overridden. Return the following list:
45
46 * the value of ``template_name`` on the view (if provided)
47 object instance that the view is operating upon (if available)
48 * ``<package_label>/<model_name><template_name_suffix>.html``
49 """
50 if self.template_name: # type: ignore
51 return [self.template_name] # type: ignore
52
53 # If template_name isn't specified, it's not a problem --
54 # we just start with an empty list.
55 names = []
56
57 # The least-specific option is the default <app>/<model>_detail.html;
58 # only use this if the object in question is a model.
59 if hasattr(self.object, "_meta"):
60 object_meta = self.object._meta
61 names.append(
62 f"{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
63 )
64
65 return names
66
67
68class DetailView(ObjectTemplateViewMixin, TemplateView):
69 """
70 Render a "detail" view of an object.
71
72 By default this is a model instance looked up from `self.queryset`, but the
73 view will support display of *any* object by overriding `self.get_object()`.
74 """
75
76 template_name_suffix = "_detail"
77
78
79class CreateView(ObjectTemplateViewMixin, FormView):
80 """
81 View for creating a new object, with a response rendered by a template.
82 """
83
84 template_name_suffix = "_form"
85
86 def post(self) -> Response:
87 """
88 Handle POST requests: instantiate a form instance with the passed
89 POST variables and then check if it's valid.
90 """
91 # Context expects self.object to exist
92 self.load_object()
93 return super().post()
94
95 def load_object(self) -> None:
96 self.object = None
97
98 # TODO? would rather you have to specify this...
99 def get_success_url(self, form):
100 """Return the URL to redirect to after processing a valid form."""
101 if self.success_url:
102 url = self.success_url.format(**self.object.__dict__)
103 else:
104 try:
105 url = self.object.get_absolute_url()
106 except AttributeError:
107 raise ImproperlyConfigured(
108 "No URL to redirect to. Either provide a url or define"
109 " a get_absolute_url method on the Model."
110 )
111 return url
112
113 def form_valid(self, form):
114 """If the form is valid, save the associated model."""
115 self.object = form.save()
116 return super().form_valid(form)
117
118
119class UpdateView(ObjectTemplateViewMixin, FormView):
120 """View for updating an object, with a response rendered by a template."""
121
122 template_name_suffix = "_form"
123
124 def post(self) -> Response:
125 """
126 Handle POST requests: instantiate a form instance with the passed
127 POST variables and then check if it's valid.
128 """
129 self.load_object()
130 return super().post()
131
132 def get_success_url(self, form):
133 """Return the URL to redirect to after processing a valid form."""
134 if self.success_url:
135 url = self.success_url.format(**self.object.__dict__)
136 else:
137 try:
138 url = self.object.get_absolute_url()
139 except AttributeError:
140 raise ImproperlyConfigured(
141 "No URL to redirect to. Either provide a url or define"
142 " a get_absolute_url method on the Model."
143 )
144 return url
145
146 def form_valid(self, form):
147 """If the form is valid, save the associated model."""
148 self.object = form.save()
149 return super().form_valid(form)
150
151 def get_form_kwargs(self):
152 """Return the keyword arguments for instantiating the form."""
153 kwargs = super().get_form_kwargs()
154 kwargs.update({"instance": self.object})
155 return kwargs
156
157
158class DeleteView(ObjectTemplateViewMixin, FormView):
159 """
160 View for deleting an object retrieved with self.get_object(), with a
161 response rendered by a template.
162 """
163
164 class EmptyDeleteForm(Form):
165 def __init__(self, instance, *args, **kwargs):
166 self.instance = instance
167 super().__init__(*args, **kwargs)
168
169 def save(self):
170 self.instance.delete()
171
172 form_class = EmptyDeleteForm
173 template_name_suffix = "_confirm_delete"
174
175 def post(self) -> Response:
176 """
177 Handle POST requests: instantiate a form instance with the passed
178 POST variables and then check if it's valid.
179 """
180 self.load_object()
181 return super().post()
182
183 def get_form_kwargs(self):
184 """Return the keyword arguments for instantiating the form."""
185 kwargs = super().get_form_kwargs()
186 kwargs.update({"instance": self.object})
187 return kwargs
188
189 def form_valid(self, form):
190 """If the form is valid, save the associated model."""
191 form.save()
192 return super().form_valid(form)
193
194
195class ListView(TemplateView):
196 """
197 Render some list of objects, set by `self.get_queryset()`, with a response
198 rendered by a template.
199 """
200
201 template_name_suffix = "_list"
202 context_object_name = "objects"
203
204 def get(self) -> Response:
205 self.objects = self.get_objects()
206 return super().get()
207
208 def get_objects(self):
209 raise NotImplementedError(
210 f"get_objects() is not implemented on {self.__class__.__name__}"
211 )
212
213 def get_template_context(self) -> dict:
214 """Insert the single object into the context dict."""
215 context = super().get_template_context() # type: ignore
216 context[self.context_object_name] = self.objects
217 return context
218
219 def get_template_names(self) -> list[str]:
220 """
221 Return a list of template names to be used for the request. May not be
222 called if render_to_response() is overridden. Return the following list:
223
224 * the value of ``template_name`` on the view (if provided)
225 object instance that the view is operating upon (if available)
226 * ``<package_label>/<model_name><template_name_suffix>.html``
227 """
228 if self.template_name: # type: ignore
229 return [self.template_name] # type: ignore
230
231 # If template_name isn't specified, it's not a problem --
232 # we just start with an empty list.
233 names = []
234
235 # The least-specific option is the default <app>/<model>_detail.html;
236 # only use this if the object in question is a model.
237 if hasattr(self.objects, "model") and hasattr(self.objects.model, "_meta"):
238 object_meta = self.objects.model._meta
239 names.append(
240 f"{object_meta.package_label}/{object_meta.model_name}{self.template_name_suffix}.html"
241 )
242
243 return names