Combining DetailView and CreateView in Django 3.2/4.0, as explained in the docs

I'm building a generic blog and trying to enable users to make comments directly on the article page. I am trying to implement this by combining DetailView with CreateView.


The docs present 3 different solutions to this issue:

  • FormMixin + DetailView: this is the answer that Django docs advise against, but that is advised by most answers on SO that I could find
  • DetailView only + write the post method: "a better solution" according to Django docs
  • DetailView + FormView: "an alternative better solution", and the one I'm trying to implement.

The "alternative better" solution consists in making a DetailView for articles and a FormView for comments, but the docs state that "This approach can also be used with any other generic class-based views", which means that DetailView + CreateView should be possible.

I've gone through a number of SO items that reference this solution, but I am unable to implement any of them.
  • This SO question suggests mixing DetailView and CreateView. However, the explanation in that answer is incomplete.
  • Another SO question, among advice to use FormMixins, has this answer that is close, but different.
  • Other questions (1, 2, etc.) only address the FormMixin and DetailView + post methods.

Here's my implementation for now:

models.py:

class Article(models.Model):
    slug = models.SlugField()
    # title, body, author
    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.slug})

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments", to_field="slug")
    body = models.TextField()
    # ...
    def get_absolute_url(self):
        return reverse("article_detail", kwargs={"slug": self.article.slug})

views.py:

class ArticleDetailView(DetailView):
    model = Article
    template_name = "article_detail.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["form"] = CommentCreateView()
        return context

class CommentCreateView(CreateView):
    """create comment"""

    model = Comment
    fields = ["body"]
    template_name = "comment_create.html"

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.article = Article.objects.filter(
            slug=self.kwargs.get("slug")
        ).first()
        self.object.author = self.request.user
        self.object.save()
        return super().form_valid(form)

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse("article_detail", kwargs={"slug": self.object.article.slug})


class ArticleCommentView(View):
    def get(self, request, *args, **kwargs):
        view = ArticleDetailView.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = CommentCreateView.as_view()
        return view(request, *args, **kwargs)

urls.py:

urlpatterns = [
    # ...
    path("article/<slug:slug>", ArticleDetailView.as_view(), name="article_detail"),
    path("comment/new", CommentCreateView.as_view(), name="comment_create"),
]

article_detail.html:

{% extends 'base.html' %}
{% block content %}
    {{ article.title }}
    {{ article.author }}
    {{ article.body }}
    <form method="post" action="{% url 'comment_create'%}">
        {% csrf_token %}
        <textarea name="{{ form.body.name }}">{{ form.body.value|default_if_none:'' }}</textarea>
        </div>
            <button type="submit">
                Post Comment
            </button>
        </div>
    </form>
    <!--  {% for comment in article.comments.all %} ...-->
{% endblock %}
Back to Top