web-dev-qa-db-fra.com

Django Passage de paramètres de formulaire personnalisés à Formset

Cela a été corrigé dans Django 1.9 avec form_kwargs .

J'ai un Django Form 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 ensemble de formulaires. Ce que je ne peux pas comprendre, c'est comment je peux transmettre les informations d'affiliation aux formulaires individuels lors de la création du jeu de formulaires. Selon les documents, pour créer un formulaire, je dois faire quelque chose comme ceci:

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

Et puis je dois le créer comme ceci:

formset = ServiceFormSet()

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

141
Paolo Bergantino

J'utiliserais functools.partial et functools.wraps :

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

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

Je pense que c'est l'approche la plus propre et n'affecte en aucun cas ServiceForm (c'est-à-dire en rendant difficile la sous-classe).

104
Carl Meyer

Document officiel Way

Django 2.0:

ArticleFormSet = formset_factory(MyArticleForm)
formset = ArticleFormSet(form_kwargs={'user': request.user})

https://docs.djangoproject.com/en/2.0/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

52
roach

Je construirais la classe de formulaire dynamiquement dans une fonction, afin 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 l'ensemble de requêtes dans le champ d'option. L'inconvénient est que le sous-classement est un peu génial. (Toute sous-classe doit être créée de la même manière.)

modifier:

En réponse à un commentaire, vous pouvez appeler cette fonction à propos de tout endroit où vous utiliseriez le nom de 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)
    ...
45
Matthew Marshall

C'est ce qui a fonctionné pour moi, Django 1.7:

from Django.utils.functional import curry    

lols = {'lols':'lols'}
formset = modelformset_factory(MyModel, form=myForm, extra=0)
formset.form = staticmethod(curry(MyForm, lols=lols))
return formset

#form.py
class MyForm(forms.ModelForm):

    def __init__(self, lols, *args, **kwargs):

J'espère que ça aide quelqu'un, ça m'a pris assez de temps pour le comprendre;)

16
rix

Je voulais placer cela comme un commentaire à la réponse de Carl Meyers, mais comme cela nécessite des points, je viens de le placer ici. Cela m'a pris 2 heures pour comprendre, donc j'espère que cela aidera quelqu'un.

Une note sur l'utilisation de l'inlineformset_factory.

J'ai utilisé cette solution moi-même et cela a fonctionné parfaitement, jusqu'à ce que je l'essaie avec l'inlineformset_factory. J'exécutais Django 1.0.2 et j'ai obtenu une étrange exception KeyError. J'ai mis à niveau vers le dernier tronc et cela a fonctionné directement.

Je peux maintenant l'utiliser comme ceci:

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

J'aime la solution de fermeture pour être "plus propre" et plus Pythonique (donc +1 à mmarshall réponse) mais Django ont également un mécanisme de rappel que vous pouvez utiliser pour filtrer les ensembles de requêtes dans les ensembles de formulaires.

Ce n'est pas non plus documenté, ce qui, à mon avis, est un indicateur que les développeurs Django pourraient ne pas aimer autant.

Donc, vous créez essentiellement votre jeu de formulaires de la même manière, mais vous ajoutez le rappel:

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

Cela crée 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. C'est un peu plus complexe de faire du rappel une méthode objet comme celle-ci, mais vous donne un peu plus de flexibilité que de faire un simple rappel de fonction.

9
Van Gale

À partir de la validation e091c18f50266097f648efc7cac2503968e9d217 le mar août 14 23:44:46 2012 +0200, la solution acceptée ne peut plus fonctionner.

La version actuelle de la fonction Django.forms.models.modelform_factory () utilise une "technique de construction de type", appelant la fonction type () sur le formulaire passé pour obtenir le type de métaclasse, puis en utilisant le résultat pour construire un objet-classe de son taper à la volée ::

# Instatiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)

Cela signifie que même un objet curryed ou partial passé à la place d'un formulaire "fait que le canard vous mord" pour ainsi dire: il appellera une fonction avec les paramètres de construction d'un ModelFormClass objet, renvoyant le message d'erreur ::

function() argument 1 must be code, not str

Pour contourner ce problème, j'ai écrit une fonction de générateur qui utilise une fermeture pour renvoyer une sous-classe de toute classe spécifiée comme premier paramètre, qui appelle ensuite super.__init__ après updateing les kwargs avec ceux fournis sur l'appel de la fonction générateur ::

def class_gen_with_kwarg(cls, **additionalkwargs):
  """class generator for subclasses with additional 'stored' parameters (in a closure)
     This is required to use a formset_factory with a form that need additional 
     initialization parameters (see http://stackoverflow.com/questions/622982/Django-passing-custom-form-parameters-to-formset)
  """
  class ClassWithKwargs(cls):
      def __init__(self, *args, **kwargs):
          kwargs.update(additionalkwargs)
          super(ClassWithKwargs, self).__init__(*args, **kwargs)
  return ClassWithKwargs

Ensuite, dans votre code, vous appellerez la fabrique de formulaires comme:

MyFormSet = inlineformset_factory(ParentModel, Model,form = class_gen_with_kwarg(MyForm, user=self.request.user))

mises en garde:

  • cela a reçu très peu de tests, du moins pour l'instant
  • les paramètres fournis peuvent heurter et écraser ceux utilisés par le code qui utilisera l'objet retourné par le constructeur
9
RobM

La solution de Carl Meyer est très élégante. J'ai essayé de l'implémenter pour modelformsets. J'avais l'impression que je ne pouvais pas appeler des méthodes statiques au sein d'une classe, mais les éléments suivants fonctionnent 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))

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

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

Le mot clé "request" est ensuite propagé à toutes les formes membres de mon jeu de formulaires. Je suis content, mais je ne sais pas pourquoi cela fonctionne - cela semble faux. Aucune suggestion?

3
trubliphone

J'ai passé un peu de temps à essayer de comprendre ce problème avant de voir cette publication.

La solution que j'ai trouvée était la solution de fermeture (et c'est une solution que j'ai utilisée auparavant avec Django model forms).

J'ai essayé la méthode curry () comme décrit ci-dessus, mais je n'arrivais pas à la faire fonctionner avec Django 1.0 donc à la fin je suis revenu à la méthode de fermeture.

La méthode de fermeture est très soignée et la seule petite bizarrerie est que la définition de classe est imbriquée dans la vue ou une autre fonction. Je pense que le fait que cela me semble étrange est un raccroc de mon expérience de programmation précédente et je pense que quelqu'un avec une formation dans des langages plus dynamiques ne bat pas une paupière!

1
Nick Craig-Wood

J'ai dû faire une chose similaire. Ceci est similaire à la solution curry:

def form_with_my_variable(myvar):
   class MyForm(ServiceForm):
     def __init__(self, myvar=myvar, *args, **kwargs):
       super(SeriveForm, self).__init__(myvar=myvar, *args, **kwargs)
   return MyForm

factory = inlineformset_factory(..., form=form_with_my_variable(myvar), ... )
1
Rory

Je suis un débutant ici, donc je ne peux pas ajouter de commentaire. J'espère que ce code fonctionnera aussi:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

ServiceFormSet.formset = staticmethod(curry(ServiceForm, affiliate=request.affiliate))

comme pour ajouter des paramètres supplémentaires au BaseFormSet du formset au lieu du formulaire.

0
Philamer Sune

basé sur cette réponse J'ai trouvé une solution plus claire:

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

    @staticmethod
    def make_service_form(affiliate):
        self.affiliate = affiliate
        return ServiceForm

Et l'exécuter en vue comme

formset_factory(form=ServiceForm.make_service_form(affiliate))
0
alexey_efimov