How loopwerk writes Django views

(loopwerk.io)

This post kept popping up over and over again. It's a different take on the right way. I vaguely remember listening to an episode of Django Chat somewhere way back when where one of the hosts, Carlton says he rights views this way.

It's almost a hybridized approach to function and class-based views. You can take on class inheritance where it makes sense, but mostly stick to the more declarative, what you see is what you get, anti-magic function-based views.

Here's an example of a class based view. This view was built to export a markdown file of a blog post.

Example 1: File export

class PostExport(SuperuserRequiredMixin, DetailView):
    context_object_name = "post"
    content_type = "text/markdown"
    http_method_names = ["get"]
    model = Post
    template_name = "post_export.md"

    def render_to_response(self, context, **response_kwargs):
        response = super().render_to_response(context, **response_kwargs)
        response["Content-Disposition"] = f'attachment; filename="{self.object.slug}.md"'

        return response

Pretty basic, nothing crazy there. Now what if what if I try to rewrite it as a basic CBV:

class PostExport(SuperuserRequiredMixin, View):
    """Returns a .md file for a given post."""

    def get(self, request, post_id: str, *args, **kwargs):
        """Response to GET requests."""

        post = get_object_or_404(Post, pk=post_id)

        response = TemplateResponse(
            request=request,
            template="post_export.md",
            context={"post": post},
            content_type="text/markdown",
        )
        response["Content-Disposition"] = f'attachment; filename="{post.slug}.md"'

        return response

Eh, kind of indifferent to be honest.

Example 2: DeleteView

Here's a really simple view to delete a post:

class PostDelete(SuperuserRequiredMixin, DeleteView):
    context_object_name = "post"
    http_method_names = ["get", "post"]
    model = Post
    success_url = reverse_lazy("archive")
    template_name = "post_view.html"

Andddd we made it a lot bigger:

class PostDelete(SuperuserRequiredMixin, View):
    def get(self, request: HttpRequest, post_id: str, *args, **kwargs) -> HttpResponse:
        """Render the template to view/display the post for a GET request."""

        return render(
            request=request,
            template_name="post_view.html",
            context={"post": get_object_or_404(Post, pk=post_id)},
        )

    def post(self, request: HttpRequest, post_id: str, *args, **kwargs) -> HttpResponse:
        """Delete the post for a POST request and redirect to another page."""

        # Delete the post
        post: Post = get_object_or_404(Post, pk=post_id)
        post.delete()
        messages.success(request=request, message=f"{post} deleted successfully!")

        # Redirect to the arvhive page
        return redirect("archive")

Example 3: "AutoSave" view

I have this view that is used to handle httpx POST request to "autosave" a post while editing it in an html form.

The original CBV looks like this:

class PostAutosave(SuperuserRequiredMixin, UpdateView):
    """
    Class-based view to handle auto-saving posts from htmx requests.
        - Successful updates return a '200' status code.
        - Invalid upsates return a '400' status code.
    """

    http_method_names = ["post"]
    context_object_name = "post"
    form_class = PostForm
    model = Post
    template_name = "post_form.html"

    def form_valid(self, form):
        # Save the form data to the object
        self.object = form.save()
        return HttpResponse(content="ok", status=HTTPStatus.OK)

    def form_invalid(self, form):
        return HttpResponse(content="error", status=HTTPStatus.BAD_REQUEST)

The updated version of the view looks like this:

class PostAutoSave(SuperuserRequiredMixin, View):
    """A view to handle httpx requests to autosave while editing a post."""

    def post(self, request: HttpRequest, post_id: str, *args, **kwargs) -> HttpResponse:
        post = get_object_or_404(Post, pk=post_id)
        form = PostForm(request.POST, instance=post)

        # If the form is valid, save the object and return an "OK" status
        if form.is_valid():
            form.save()
            return HttpResponse(content="ok", status=HTTPStatus.OK)

        # Return BAD_REQUEST if the form wasn't valid
        return HttpResponse(content="error", status=HTTPStatus.BAD_REQUEST)

Looking at these side-by-side, I'm kind of indifferent. But, creating that first version of the UpdateView and digging through the various layers of inheritance was a lot more tedious.

Conclusion

I don't think I'll go and rewrite my entire project anytime soon, but for new views I'll probably follow these rules:

  1. If the view uses the "out-of-the-box" generic CBV functionality --> Use the generic CBV.
  2. If the view requires some customization --> Use the 'View' CBV.