146 votes

Django transmet les paramètres d'un formulaire personnalisé à un ensemble de formulaires

J'ai un formulaire Django qui ressemble à ceci :

class ServiceForm(forms.Form):
    option = forms.ModelChoiceField(queryset=ServiceOption.objects.none())
    rate = forms.DecimalField(widget=custom_widgets.SmallField())
    units = forms.IntegerField(min_value=1, widget=custom_widgets.SmallField())

    def __init__(self, *args, **kwargs):
        affiliate = kwargs.pop('affiliate')
        super(ServiceForm, self).__init__(*args, **kwargs)
        self.fields["option"].queryset = ServiceOption.objects.filter(affiliate=affiliate)

J'appelle ce formulaire avec quelque chose comme ceci :

form = ServiceForm(affiliate=request.affiliate)

request.affiliate est l'utilisateur connecté. Cela fonctionne comme prévu.

Mon problème est que je veux maintenant transformer ce formulaire unique en un jeu de formulaires. Ce que je n'arrive pas à comprendre, c'est comment transmettre les informations d'affiliation aux formulaires individuels lors de la création du jeu de formulaires. D'après la documentation, pour créer un jeu de formulaires, je dois faire quelque chose comme ceci :

ServiceFormSet = forms.formsets.formset_factory(ServiceForm, extra=3)

Et ensuite je dois le créer comme ceci :

formset = ServiceFormSet()

Maintenant, comment puis-je passer affiliate=request.affiliate aux formulaires individuels de cette façon ?

106voto

Carl Meyer Points 30736

J'utiliserais functools.partial y functools.wraps :

from functools import partial, wraps
from django.forms.formsets import formset_factory

ServiceFormSet = formset_factory(wraps(ServiceForm)(curry(ServiceForm, affiliate=request.affiliate)), extra=3)

Je pense que c'est l'approche la plus propre, et qu'elle n'affecte pas ServiceForm de quelque manière que ce soit (c'est-à-dire en rendant difficile la sous-classe).

45voto

Matthew Marshall Points 2521

Je construirais la classe de formulaire dynamiquement dans une fonction, de sorte qu'elle ait accès à l'affilié via la fermeture :

def make_service_form(affiliate):
    class ServiceForm(forms.Form):
        option = forms.ModelChoiceField(
                queryset=ServiceOption.objects.filter(affiliate=affiliate))
        rate = forms.DecimalField(widget=custom_widgets.SmallField())
        units = forms.IntegerField(min_value=1, 
                widget=custom_widgets.SmallField())
    return ServiceForm

En prime, vous n'avez pas à réécrire le queryset dans le champ d'option. L'inconvénient est que la sous-classification est un peu funky. (Toute sous-classe doit être faite d'une manière similaire).

éditer :

En réponse à un commentaire, vous pouvez appeler cette fonction à peu près partout où vous utiliseriez le nom de la classe :

def view(request):
    affiliate = get_object_or_404(id=request.GET.get('id'))
    formset_cls = formset_factory(make_service_form(affiliate))
    formset = formset_cls(request.POST)
    ...

9voto

Van Gale Points 21982

J'aime bien la solution de la fermeture parce qu'elle est plus "propre" et plus pythique (donc +1 à la réponse de mmarshall) mais les formulaires Django ont aussi un mécanisme de rappel que vous pouvez utiliser pour filtrer les querysets dans les formsets.

Il n'est pas non plus documenté, ce qui, à mon avis, indique que les développeurs de Django ne l'apprécient peut-être pas autant.

Vous créez donc votre formset de la même manière, mais vous ajoutez le callback :

ServiceFormSet = forms.formsets.formset_factory(
    ServiceForm, extra=3, formfield_callback=Callback('option', affiliate).cb)

Il s'agit de créer une instance d'une classe qui ressemble à ceci :

class Callback(object):
    def __init__(self, field_name, aff):
        self._field_name = field_name
        self._aff = aff
    def cb(self, field, **kwargs):
        nf = field.formfield(**kwargs)
        if field.name == self._field_name:  # this is 'options' field
            nf.queryset = ServiceOption.objects.filter(affiliate=self._aff)
        return nf

Cela devrait vous donner une idée générale. Il est un peu plus complexe de faire du callback une méthode objet comme celle-ci, mais cela vous donne un peu plus de flexibilité par rapport à un simple callback de fonction.

9voto

Johan Nilsson Points 1224

Je voulais placer ceci comme commentaire à la réponse de Carl Meyers, mais comme cela nécessite des points, je l'ai juste placé ici. J'ai mis deux heures à trouver cette réponse et j'espère qu'elle aidera quelqu'un.

Une remarque sur l'utilisation de l'inlineformset_factory.

J'ai utilisé cette solution moi-même et cela fonctionnait parfaitement, jusqu'à ce que je l'essaie avec le inlineformset_factory. J'exécutais Django 1.0.2 et j'ai eu une exception KeyError étrange. J'ai fait une mise à jour vers le dernier tronc et ça a marché directement.

Je peux maintenant l'utiliser comme ceci :

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))

3voto

trubliphone Points 449

La solution de Carl Meyer semble très élégante. J'ai essayé de l'implémenter pour les modelformsets. J'avais l'impression que je ne pouvais pas appeler les staticmethods dans une classe, mais ce qui suit fonctionne inexplicablement :

class MyModel(models.Model):
  myField = models.CharField(max_length=10)

class MyForm(ModelForm):
  _request = None
  class Meta:
    model = MyModel

    def __init__(self,*args,**kwargs):      
      self._request = kwargs.pop('request', None)
      super(MyForm,self).__init__(*args,**kwargs)

class MyFormsetBase(BaseModelFormSet):
  _request = None

def __init__(self,*args,**kwargs):
  self._request = kwargs.pop('request', None)
  subFormClass = self.form
  self.form = curry(subFormClass,request=self._request)
  super(MyFormsetBase,self).__init__(*args,**kwargs)

MyFormset =  modelformset_factory(MyModel,formset=MyFormsetBase,extra=1,max_num=10,can_delete=True)
MyFormset.form = staticmethod(curry(MyForm,request=MyFormsetBase._request))

A mon avis, si je fais quelque chose comme ça :

formset = MyFormset(request.POST,queryset=MyModel.objects.all(),request=request)

Ensuite, le mot clé "request" est propagé à tous les formulaires membres de mon jeu de formulaires. Je suis satisfait, mais je n'ai aucune idée de la raison pour laquelle cela fonctionne - cela semble erroné. Avez-vous des suggestions ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X