Plain is headed towards 1.0! Subscribe for development updates →

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