Django: Implementing a Form within a generic DetailView
Asked Answered
N

3

21

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
Nostalgia answered 13/8, 2017 at 11:46 Comment(0)
P
31

Use FormMixin if you want combine DetailView and a form:

from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic.detail import DetailView
from django.views.generic.edit import FormMixin
from django.urls import reverse

from .models import Post, Comment
from .forms import CommentForm


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

    def get_success_url(self):
        return reverse('post_detail', kwargs={'pk': self.object.id})

    def get_context_data(self, **kwargs):
        context = super(ParticularPost, self).get_context_data(**kwargs)
        context['form'] = CommentForm(initial={'post': self.object})
        return context

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        form.save()
        return super(ParticularPost, self).form_valid(form)

And don't forget to add the post field into the form (you can do it hidden):

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('author', 'text', 'post',)

And the better way to add a creation date - use auto_now_add=True:

created_date = models.DateTimeField(auto_now_add=True)
Presidency answered 13/8, 2017 at 15:29 Comment(7)
May be of use: docs.djangoproject.com/en/1.11/topics/class-based-views/mixins/…Hedden
Note that the docs advice against this pattern (as per @pjdavis)Reinaldoreinaldos
Based on the sourcecode here github.com/django/django/blob/master/django/views/generic/…, I believe we can just inherit from BaseFormView instead of FormMixin, since the post method has already been written.Mescaline
I get object has no attribute 'get_form'Dam
Instead of FormMixin I used ModelFormMixin. #66504203Dam
But I get The page that you're looking for used information that you entered. Returning to that page might cause any action that you took to be repeated. Do you want to continue?Dam
It doesn't save for me.Dam
M
2

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',)
        }
Mechanotherapy answered 16/6, 2022 at 1:17 Comment(0)
B
0

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)
Bottomry answered 1/5, 2021 at 19:17 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.