[Django]-Django: Implementing a Form within a generic DetailView

30👍

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)

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',)
        }

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)

Leave a comment