Plain is headed towards 1.0! Subscribe for development updates →

  1# Views
  2
  3**Take a request, return a response.**
  4
  5Plain views are written as classes,
  6with a straightforward API that keeps simple views simple,
  7but gives you the power of a full class to handle more complex cases.
  8
  9```python
 10from plain.views import View
 11
 12
 13class ExampleView(View):
 14    def get(self):
 15        return "<html><body>Hello, world!</body></html>"
 16```
 17
 18## HTTP methods -> class methods
 19
 20The HTTP methd of the request will map to a class method of the same name on the view.
 21
 22If a request comes in and there isn't a matching method on the view,
 23Plain will return a `405 Method Not Allowed` response.
 24
 25```python
 26from plain.views import View
 27
 28
 29class ExampleView(View):
 30    def get(self):
 31        pass
 32
 33    def post(self):
 34        pass
 35
 36    def put(self):
 37        pass
 38
 39    def patch(self):
 40        pass
 41
 42    def delete(self):
 43        pass
 44
 45    def trace(self):
 46        pass
 47```
 48
 49The [base `View` class](./base.py) defines default `options` and `head` behavior,
 50but you can override these too.
 51
 52## Return types
 53
 54For simple JSON responses, HTML, or status code responses,
 55you don't need to instantiate a `Response` object.
 56
 57```python
 58class JsonView(View):
 59    def get(self):
 60        return {"message": "Hello, world!"}
 61
 62
 63class HtmlView(View):
 64    def get(self):
 65        return "<html><body>Hello, world!</body></html>"
 66
 67
 68class StatusCodeView(View):
 69    def get(self):
 70        return 204  # No content
 71```
 72
 73## Template views
 74
 75The most common behavior for a view is to render a template.
 76
 77```python
 78from plain.views import TemplateView
 79
 80
 81class ExampleView(TemplateView):
 82    template_name = "example.html"
 83
 84    def get_template_context(self):
 85        context = super().get_template_context()
 86        context["message"] = "Hello, world!"
 87        return context
 88```
 89
 90The `TemplateView` is also the base class for _most_ of the other built-in view classes.
 91
 92Template views that don't need any custom context can use `TemplateView.as_view()` direcly in the URL route.
 93
 94```python
 95from plain.views import TemplateView
 96from plain.urls import path, Router
 97
 98
 99class AppRouter(Router):
100    routes = [
101        path("/example/", TemplateView.as_view(template_name="example.html")),
102    ]
103```
104
105## Form views
106
107Standard [forms](../forms) can be rendered and processed by a `FormView`.
108
109```python
110from plain.views import FormView
111from .forms import ExampleForm
112
113
114class ExampleView(FormView):
115    template_name = "example.html"
116    form_class = ExampleForm
117    success_url = "."  # Redirect to the same page
118
119    def form_valid(self, form):
120        # Do other successfull form processing here
121        return super().form_valid(form)
122```
123
124Rendering forms is done directly in the HTML.
125
126```html
127{% extends "base.html" %}
128
129{% block content %}
130
131<form method="post">
132    {{ csrf_input }}
133
134    <!-- Render general form errors -->
135    {% for error in form.non_field_errors %}
136    <div>{{ error }}</div>
137    {% endfor %}
138
139    <!-- Render form fields individually (or with Jinja helps or other concepts) -->
140    <label for="{{ form.email.html_id }}">Email</label>
141    <input
142        type="email"
143        name="{{ form.email.html_name }}"
144        id="{{ form.email.html_id }}"
145        value="{{ form.email.value() or '' }}"
146        autocomplete="email"
147        autofocus
148        required>
149    {% if form.email.errors %}
150    <div>{{ form.email.errors|join(', ') }}</div>
151    {% endif %}
152
153    <button type="submit">Save</button>
154</form>
155
156{% endblock %}
157```
158
159## Object views
160
161The object views support the standard CRUD (create, read/detail, update, delete) operations, plus a list view.
162
163```python
164from plain.views import DetailView, CreateView, UpdateView, DeleteView, ListView
165
166
167class ExampleDetailView(DetailView):
168    template_name = "detail.html"
169
170    def get_object(self):
171        return MyObjectClass.objects.get(
172            pk=self.url_kwargs["pk"],
173            user=self.request.user,  # Limit access
174        )
175
176
177class ExampleCreateView(CreateView):
178    template_name = "create.html"
179    form_class = CustomCreateForm
180    success_url = "."
181
182
183class ExampleUpdateView(UpdateView):
184    template_name = "update.html"
185    form_class = CustomUpdateForm
186    success_url = "."
187
188    def get_object(self):
189        return MyObjectClass.objects.get(
190            pk=self.url_kwargs["pk"],
191            user=self.request.user,  # Limit access
192        )
193
194
195class ExampleDeleteView(DeleteView):
196    template_name = "delete.html"
197    success_url = "."
198
199    # No form class necessary.
200    # Just POST to this view to delete the object.
201
202    def get_object(self):
203        return MyObjectClass.objects.get(
204            pk=self.url_kwargs["pk"],
205            user=self.request.user,  # Limit access
206        )
207
208
209class ExampleListView(ListView):
210    template_name = "list.html"
211
212    def get_objects(self):
213        return MyObjectClass.objects.filter(
214            user=self.request.user,  # Limit access
215        )
216```
217
218## Response exceptions
219
220At any point in the request handling,
221a view can raise a `ResponseException` to immediately exit and return the wrapped response.
222
223This 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.
224
225```python
226from plain.views import DetailView
227from plain.views.exceptions import ResponseException
228from plain.http import Response
229
230
231class ExampleView(DetailView):
232    def get_object(self):
233        if self.request.user.exceeds_rate_limit:
234            raise ResponseException(
235                Response("Rate limit exceeded", status_code=429)
236            )
237
238        return AnExpensiveObject()
239```
240
241## Error views
242
243By default, HTTP errors will be rendered by `templates/<status_code>.html` or `templates/error.html`.
244
245You can define your own error views by pointing the `HTTP_ERROR_VIEWS` setting to a dictionary of status codes and view classes.
246
247```python
248# app/settings.py
249HTTP_ERROR_VIEWS = {
250    404: "errors.NotFoundView",
251}
252```
253
254```python
255# app/errors.py
256from plain.views import View
257
258
259class NotFoundView(View):
260    def get(self):
261        # A custom implementation or error view handling
262        pass
263```
264
265## Redirect views
266
267```python
268from plain.views import RedirectView
269
270
271class ExampleRedirectView(RedirectView):
272    url = "/new-location/"
273    permanent = True
274```
275
276Redirect views can also be used in the URL router.
277
278```python
279from plain.views import RedirectView
280from plain.urls import path, Router
281
282
283class AppRouter(Router):
284    routes = [
285        path("/old-location/", RedirectView.as_view(url="/new-location/", permanent=True)),
286    ]
287```
288
289## CSRF exempt views
290
291```python
292from plain.views import View
293from plain.views.csrf import CsrfExemptViewMixin
294
295
296class ExemptView(CsrfExemptViewMixin, View):
297    def post(self):
298        return "Hello, world!"
299```