Django: Implementing a Form within a generic DetailView

Viewed 11875

After working through several google search result pages, I am still desperately stuck at the same problem. I am trying to implement a comment field underneath a blog post. I am thankful for any hints and advice!

I am working on a Blog in Django which is set up with a first, generic ListView to display briefly all available blog posts and with a second, generic DetailView to show the specific blog post in more detail. I now want to place an add_comment_field underneath the specific blog post with all other comments shown underneath. It works when the comment form is displayed on a separate page but not on the same page as the DetailView, which is the desired outcome.

I suspect this has to do with the interplay between views.py and forms.py but I cannot figure out the problem.

Again, thank you so much for your help!

views.py

from django.shortcuts import render, get_object_or_404, redirect
from .models import Post, Comment
from .forms import CommentForm
from django.views.generic.detail import DetailView

class ParticularPost(DetailView):
    template_name='blog/post.html'
    model = Post

    def add_comment_to_post(self, pk):
        post = get_object_or_404(Post, pk=pk)
        if self.method == "POST":
            form = CommentForm(self.POST)
            if form.is_valid():
                comment = form.save(commit=False)
                comment.post = post
                comment.save()
                return redirect('post_detail', pk=post.pk)
        else:
            form = CommentForm()
        return {'form': form}

urls.py

from django.conf.urls import url, include
from django.views.generic import ListView, DetailView
from .models import Post, Comment
from .views import ParticularPost

urlpatterns = [
    url(r'^$', ListView.as_view(queryset=Post.objects.all().order_by("-date")[:25], template_name="blog/blog.html")),
    url(r'^(?P<pk>\d+)$', ParticularPost.as_view(), name="post_detail"),
]

post.html

{% extends "personal/header.html" %}
{% load staticfiles %}
{% block content %}
<div class="container-fluid background_design2 ">
    <div class="header_spacing"></div>
    <div class="container post_spacing">
        <div class="row background_design1 blog_post_spacing inline-headers">
            <h3><a href="/blog/{{post.id}}">{{ post.title }}</a></h3>
            <h6> on {{ post.date }}</h6>
            <div class = "blog_text">
                {{ post.body|safe|linebreaks}}
            </div>
            <br><br>
        </div>
        <div>
            <form method="POST" class="post-form">{% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="save btn btn-default">Send</button>
    </form>
        </div>
        <div class=" row post_spacing background_design1 ">
            <hr>
            {% for comment in post.comments.all %}
                <div class=" col-md-12 comment">
                    <div class="date">{{ comment.created_date }}</div>
                    <strong>{{ comment.author }}</strong>
                    <p>{{ comment.text|linebreaks }}</p>
                </div>
            {% empty %}
                <p>No comments here yet :(</p>
            {% endfor %}
        </div>
    </div>
</div>
{% endblock %}

forms.py

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = ('author', 'text',)

models.py

from django.db import models
from django.utils import timezone

class Post(models.Model):
    title = models.CharField(max_length=140)
    body = models.TextField()
    date = models.DateTimeField()

    def __str__(self):
        return self.title

class Comment(models.Model):
    post = models.ForeignKey('blog.Post', related_name='comments')
    author = models.CharField(max_length=200)
    text = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.text
4 Answers

It's not necessary to populate the form with initial. I will extend the above solution.

def form_valid(self, form):
     post = self.get_object()
     myform = form.save(commit=False)
     myform.post =  post
     form.save()
     return super(ParticularPost, self).form_valid(form)

Is there a way to hide the form once a user has added a comment? This works for function based views by passing on a variable (e.g. new_comment = True) and with an if clause in the template. But can't seem to figure it out for class-based-views. Even if you set the variable to true in the form_valid() function, the form still pops up.

As several people have mentioned in the comments for Anton Shurashov's answer, while the solution provided works it is not the solution that the devs recommend in the Django docs.

I followed the alternate solution given in the docs for a project that seems quite similar to OP's. Hopefully this solution will be useful for anyone else trying to solve this same problem.

First I created DetailView and defined my own get_context_data method to add the form to the context:

from django.shortcuts import render
from django.views import View
from django.views.generic import ListView, DetailView
from django.views.generic.edit import FormView
from django.views.generic.detail import SingleObjectMixin
from django.http import Http404, HttpResponseForbidden

from .models import BlogPost, Comment

from users.models import BlogUser
from .forms import CommentForm

class BlogPostDetailView(DetailView):
    """
    Shows each individual blog post 
    and relevant information. 
    """
    model = BlogPost
    template_name = 'blog/blogpost_detail.html'
    context_object_name = 'blog_post'

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

Then in my FormView view, I defined the post method to be run when a user submits the form (adds a new comment). (One note, I set the success_url = '#' so that the form would stay on the same page. There are a myriad of ways to accomplish this, but this was the easiest one for me.):

class CommentFormView(SingleObjectMixin, FormView):
    """
    View for the comment form, which allows users to 
    leave comments user a blog post if logged in.
    """
    template_name = 'blog/blogpost_detail.html'
    form_class = CommentForm
    model = Comment
    success_url = '#'

    def post(self, request, *args, **kwargs):
        """
        Posts the comment only if the user is logged in.
        """
        if not request.user.is_authenticated:
            return HttpResponseForbidden()
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)

The final View brings everything together, and is a simple View, where the get method calls the BlogPostDetailView (Detail View) and the post method calls the CommentFormView.

Within the post method I also create a form object to automatically set the current user to the author of the comment and the blog post to the current blog post that the page is showing.

class PostView(View):

    def get(self, request, *args, **kwargs):
        view = BlogPostDetailView.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = CommentFormView.as_view()
        form = CommentForm(request.POST)
        # Set the current user
        # to the comment_author field
        form.instance.comment_author = request.user
        # Set the blog post as the current blogpost
        form.instance.post = BlogPost.objects.get(id=self.kwargs['pk'])
        if form.is_valid():
            form.save()
        return view(request, *args, **kwargs)

In my forms.py I have defined my CommentForm Model like so (I set the label to an empty string so that the label 'content' did not show up above the new comment):

from django import forms
from ckeditor.widgets import CKEditorWidget

from .models import Comment


class CommentForm(forms.ModelForm):
    """
    Gives the option to add a comment to the bottom of a Blog Post, 
    but only for logged in users.
    """
    content = forms.CharField(widget=CKEditorWidget(), label='')
    class Meta:
        model = Comment
        fields = [ 'content',]
    
    class Media:
        css = {
            'all': ('forms.css',)
        }
Related