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:
- If the view uses the "out-of-the-box" generic CBV functionality --> Use the generic CBV.
- If the view requires some customization --> Use the 'View' CBV.