web-dev-qa-db-fra.com

Django combine DetailView et FormView

J'ai une vue où j'ai besoin d'afficher des informations sur une instance de modèle donnée et j'utilise donc une variable DetailView. J'ai également besoin de cette même vue pour gérer un formulaire standard (pas un formulaire modèle), affichant le formulaire sur GET et le validant sur POST. Pour ce faire, j'essaie d'utiliser une FormView mais la combinaison des deux classes de vues ne fonctionne pas:

class FooView(FormView, DetailView):
    # configs here

Dans GET (pour simplifier la question, je ne montrerai que le problème avec GET car POST a un problème différent), cela ne fonctionne pas car le formulaire n'est jamais ajouté au contexte. La raison en est liée à l'ordre de résolution de la méthode pour cette classe:

>>> 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)

Dans la demande, Django doit récupérer le formulaire et l'ajouter au contexte. Cela se passe dans 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))

Cependant, la première classe avec le MRO qui a défini get est 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)

Comme vous pouvez le voir, le BaseDetailView.get n'appelle jamais super et donc le ProcessFormView.get ne sera jamais appelé, de sorte que le formulaire ne sera pas ajouté au contexte. Cela peut être corrigé en créant une vue mixte où toutes ces nuances pour GET et POST peuvent être prises en charge, mais je ne pense pas que ce soit une solution propre.

Ma question: est-il possible d'accomplir ce que je veux avec l'implémentation CBV par défaut de Django sans créer de mixins?

17
miki725

Une solution consisterait à utiliser mixins, comme indiqué dans le commentaire de limelights ci-dessus.

Une autre approche consiste à avoir deux vues distinctes, l’une a DetailView et l’autre a FormView. Ensuite, dans le modèle pour le premier, affichez le même formulaire que celui que vous utilisez dans le dernier, à ceci près que vous ne laisserez pas l'attribut action vide. Réglez-le plutôt sur l'URL de FormView. Quelque chose dans le sens de ceci (s'il vous plaît méfiez-vous des erreurs que j'écris ceci sans aucun test):

Dans 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'

Dans my_detail_view.html:

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

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

{{ form }}

</form>

Dans 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'),
# ...

Notez que le décorateur require_POST est facultatif, dans le cas où vous ne souhaitez pas que MyFormView soit accessible par GET et que vous ne souhaitiez le traiter que lorsque le formulaire est soumis.

26
Berislav Lopac

Django a également une documentation assez longue sur ce problème.

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

Ils conseillent de créer 2 vues différentes et de faire en sorte que la vue de détail se réfère à la vue de formulaire sur le post.

Je vois actuellement si ce hack pourrait fonctionner:

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)
11
dyve

En utilisant 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'),
]

modèle

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>
2
Admir

Dans Django By Example de lightbird, ils utilisent une bibliothèque, MCBV, pour mélanger des vues génériques:

Les tutoriels de mon guide utiliseront une bibliothèque de vues basées sur des classes basées sur des vues génériques Django modifiées; la bibliothèque est appelée MCBV (M = modulaire) et la principale différence par rapport aux CBV génériques est qu’il est possible de mélanger et de faire correspondre facilement plusieurs vues génériques (par exemple, ListView et CreateView, DetailView et UpdateView, etc.)

Vous pouvez suivre l'explication ici: helper-functions

Et utilisez-le pour mélanger FormView et DetailView, ou autre chose

Code: MCBV

1
lborgav

J'ai effectué ma solution en utilisant ModelForms et quelque chose comme ceci: Sur la méthode get_context_data de mon DetailView j'ai créé:

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

Et mon FormView était comme:

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)
0
Gregory