24

I have a view where I need to display information about a certain model instance hence I use a DetailView. I also need that same view to handle a regular form (not a model form), both displaying the form on GET and validating it on POST. To do that, I am trying to use a FormView however the combination of both view clases does not work:

class FooView(FormView, DetailView):
    # configs here

In GET (for simplicity of the question I will only show the issue with GET since POST has a different issue), it does not work because the form never gets added to the context. The reason has to do with method resolution order for that class:

>>> inspect.getmro(FooView)
(FooView,
 django.views.generic.edit.FormView,
 django.views.generic.detail.DetailView,
 django.views.generic.detail.SingleObjectTemplateResponseMixin,
 django.views.generic.base.TemplateResponseMixin,
 django.views.generic.edit.BaseFormView,
 django.views.generic.edit.FormMixin,
 django.views.generic.detail.BaseDetailView,
 django.views.generic.detail.SingleObjectMixin,
 django.views.generic.base.ContextMixin,
 django.views.generic.edit.ProcessFormView,
 django.views.generic.base.View,
 object)

Within the request, Django has to get the form and add it to the context. That happens in ProcessFormView.get:

def get(self, request, *args, **kwargs):
    """
    Handles GET requests and instantiates a blank version of the form.
    """
    form_class = self.get_form_class()
    form = self.get_form(form_class)
    return self.render_to_response(self.get_context_data(form=form))

However the first class with the MRO which has get defined is BaseDetailView:

def get(self, request, *args, **kwargs):
    self.object = self.get_object()
    context = self.get_context_data(object=self.object)
    return self.render_to_response(context)

As you can see the BaseDetailView.get never calls super hence the ProcessFormView.get will never be called hence the the form will not be added to the context. This can be fixed by creating a mixin view where all these nuances for GET and POST can be taken care of however I do not feel it is a clean solution.

My question: is there any way of accomplishing what I want with Django's default CBV implementation without creating any mixins?

miki725
  • 27,207
  • 17
  • 105
  • 121

6 Answers6

37

One solution would be to use mixins, as per limelights' comment above.

Another approach is to have two separate views, one a DetailView and the other a FormView. Then, in the template for the former, display the same form you're using in the latter, except that you won't leave the action attribute empty -- instead, set it to the url for the FormView. Something along the lines of this (please beware of any errors as I'm writing this without any testing):

In views.py:

class MyDetailView(DetailView):
    model = MyModel
    template_name = 'my_detail_view.html'

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

class MyFormView(FormView):
    form_class = MyFormClass
    success_url = 'go/here/if/all/works'

In my_detail_view.html:

<!-- some representation of the MyModel object -->

<form method="post" action="{% url "my_form_view_url" %}">

{{ form }}

</form>

In urls.py:

# ...
url('^my_model/(?P<pk>\d+)/$', MyDetailView.as_view(), name='my_detail_view_url'),
url('^my_form/$', require_POST(MyFormView.as_view()), name='my_form_view_url'),
# ...

Note that the require_POST decorator is optional, in the case that you don't want the MyFormView to be accessible by GET and want it only to be processed when the form is submitted.

Berislav Lopac
  • 16,656
  • 6
  • 71
  • 80
20

Django also has a rather lengthy documentation about this problem.

https://docs.djangoproject.com/en/1.8/topics/class-based-views/mixins/#using-formmixin-with-detailview

They advise to make 2 different views, and have the detail view refer to the form view on post.

I'm currently seeing if this hack might work:

class MyDetailFormView(FormView, DetailView):
    model = MyModel
    form_class = MyFormClass
    template_name = 'my_template.html'

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

    def post(self, request, *args, **kwargs):
        return FormView.post(self, request, *args, **kwargs)
dyve
  • 5,893
  • 2
  • 30
  • 44
7

By using FormMixin

views.py

from django.contrib.auth import get_user_model
from django.core.urlresolvers import (
    reverse_lazy
    )
from django.http import Http404
from django.shortcuts import (
    render,
    redirect
    )
from django.views.generic import (
    DetailView,
    FormView,
    )
from django.views.generic.edit import FormMixin    

from .forms import SendRequestForm


User = get_user_model()  


class ViewProfile(FormMixin, DetailView):

    model = User
    context_object_name = 'profile'
    template_name = 'htmls/view_profile.html'
    form_class = SendRequestForm

    def get_success_url(self):
        return reverse_lazy('view-profile', kwargs={'pk': self.object.pk})

    def get_object(self):
        try:
            my_object = User.objects.get(id=self.kwargs.get('pk'))
            return my_object
        except self.model.DoesNotExist:
            raise Http404("No MyModel matches the given query.")

    def get_context_data(self, *args, **kwargs):
        context = super(ViewProfile, self).get_context_data(*args, **kwargs)
        profile = self.get_object()
        # form
        context['form'] = self.get_form()
        context['profile'] = profile
        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):
    #put logic here
        return super(ViewProfile, self).form_valid(form)

    def form_invalid(self, form):
    #put logic here
        return super(ViewProfile, self).form_invalid(form)

forms.py

from django import forms 

class SendRequestForm(forms.Form):

    request_type = forms.CharField()

    def clean_request_type(self):
        request_type = self.cleaned_data.get('request_type')
        if 'something' not in request_type:
            raise forms.ValidationError('Something must be in request_type field.')
        return request_type

urls.py

urlpatterns = [
    url(r'^view-profile/(?P<pk>\d+)', ViewProfile.as_view(), name='view-profile'),
]

template

username -{{object.username}}
id -{{object.id}}
<form action="{% url 'view-profile' object.id %}" method="POST">
    {% csrf_token %}
    {{form}}
    <input type="submit" value="Send request">
</form>
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Admir
  • 2,876
  • 1
  • 19
  • 19
1

In Django By Example from lightbird, they're using a library, MCBV, to mix generic views:

My guide’s tutorials will use a library of class-based views based on modified Django generic views; the library is called MCBV (M stands for modular) and the main difference compared to generic CBVs is that it’s possible to mix and match multiple generic views easily (e.g. ListView and CreateView, DetailView and UpdateView, etc.)

You can follow the explanation here: helper-functions

And use it to mix FormView and DetailView, or whatever

Code: MCBV

lborgav
  • 2,371
  • 19
  • 28
0

I performed my solution using ModelForms and something like this: On method get_context_data of my DetailView I made:

form = CommentForm(
        instance=Comment(
            school=self.object, user=self.request.user.profile
        )
    )
    context['form'] = form

And my FormView was like:

class SchoolComment(FormView):
form_class = CommentForm

def get_success_url(self):
    return resolve_url('schools:school-profile', self.kwargs.get('pk'))

def form_valid(self, form):
    form.save()
    return super(SchoolComment, self).form_valid(form)
Gregory
  • 6,514
  • 4
  • 28
  • 26
0

That's a old post but good for reference.

One elegant and reusable away is to use a custom inclusion tag for the form.

templatetags/my_forms_tag.py

from django import template
from ..forms import MyFormClass

register = template.Library()

@register.inclusion_tag('<app>\my_form.html')
def form_tag(*args, **kwargs):
    my_form = MyFormClass()

    return {'my_form ':my_form}

my_form.html

<form method="post" action="{% url "my_form_view_url" %}">

{{ form }}

</form>

The post will be taken by FormView werever view you put inclusion tag. And it can receive any context you pass in the inclusion. Dont forget to load my_form_tag and create the view for MyForm and include the entry in urls.py