Django passe des paramètres de formulaire personnalisés à Formset


150

Ce problème a été corrigé dans Django 1.9 avec form_kwargs .

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.affiliateest l'utilisateur connecté. Cela fonctionne comme prévu.

Mon problème est que je veux maintenant transformer ce formulaire unique en un formset. 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 formset. Selon la documentation, pour créer un formulaire à partir de cela, je dois faire quelque chose comme ceci:

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

Et puis j'ai besoin de le créer comme ceci:

formset = ServiceFormSet()

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

Réponses:


105

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 aucune façon ServiceForm (c'est-à-dire en rendant difficile la sous-classe).


Ça ne fonctionne pas pour moi. J'obtiens l'erreur: AttributeError: l'objet '_curriedFormSet' n'a pas d'attribut 'get'
Paolo Bergantino

Je ne peux pas dupliquer cette erreur. C'est aussi étrange car un formset n'a généralement pas d'attribut 'get', il semble donc que vous fassiez quelque chose d'étrange dans votre code. (De plus, j'ai mis à jour la réponse avec un moyen de se débarrasser des bizarreries comme '_curriedFormSet').
Carl Meyer

Je revisite ceci parce que j'aimerais que votre solution fonctionne. Je peux déclarer le formset bien, mais si j'essaye de l'imprimer en faisant {{formset}}, c'est quand j'obtiens l'erreur "has no attribute 'get'". Cela se produit avec l'une ou l'autre des solutions que vous avez fournies. Si je boucle sur l'ensemble de formulaires et imprime les formulaires sous la forme {{form}}, j'obtiens à nouveau l'erreur. Si je boucle et imprime comme {{form.as_table}} par exemple, j'obtiens des tables de formulaire vides, ie. aucun champ n'est imprimé. Des idées?
Paolo Bergantino

Vous avez raison, je suis désolé; mes tests précédents ne sont pas allés assez loin. J'ai retrouvé cela, et cela se brise en raison de certaines bizarreries dans la façon dont les FormSets fonctionnent en interne. Il existe un moyen de contourner le problème, mais il commence à perdre l'élégance d'origine ...
Carl Meyer

5
Si le fil de commentaires ici n'a pas de sens, c'est parce que je viens de modifier la réponse pour utiliser Python au functools.partiallieu de Django django.utils.functional.curry. Ils font la même chose, sauf que functools.partialretourne un type appelable distinct au lieu d'une fonction Python régulière, et le partialtype ne se lie pas en tant que méthode d'instance, ce qui résout parfaitement le problème que ce fil de commentaires était largement consacré au débogage.
Carl Meyer

81

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


8
cela devrait être la bonne façon de le faire maintenant. la réponse acceptée fonctionne et est agréable mais est un hack
Junchao Gu

certainement la meilleure réponse et la bonne façon de le faire.
yaniv14


46

Je créerais 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 le jeu de requête 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.)

Éditer:

En réponse à un commentaire, vous pouvez appeler cette fonction à tout endroit 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)
    ...

Merci - cela a fonctionné. Je me retiens de marquer cela comme accepté parce que j'espère un peu qu'il y a une option plus propre, car le faire de cette façon semble vraiment génial.
Paolo Bergantino

Marquer comme accepté car apparemment c'est la meilleure façon de le faire. Ça fait bizarre, mais ça fait l'affaire. :) Je vous remercie.
Paolo Bergantino

Carl Meyer a, je pense, la façon la plus propre que vous recherchiez.
Jarret Hardie

J'utilise cette méthode avec Django ModelForms.
chefsmart

J'aime cette solution, mais je ne sais pas comment l'utiliser dans une vue comme un formset. Avez-vous de bons exemples d'utilisation de cela dans une vue? Toutes les suggestions sont appréciées.
Joe J

16

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 cela aide quelqu'un, cela m'a pris assez de temps pour le comprendre;)


1
Pouvez-vous m'expliquer pourquoi staticmethodest nécessaire ici?
fpghost

9

J'aime la solution de fermeture pour être «plus propre» et plus pythonique (donc +1 à la réponse mmarshall) mais les formulaires Django ont aussi 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, je pense, est un indicateur que les développeurs de Django pourraient ne pas l'aimer autant.

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

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

Ceci 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 callback une méthode objet comme celle-ci, mais vous donne un peu plus de flexibilité que de faire un simple rappel de fonction.


1
Merci pour la réponse. J'utilise la solution de mmarshall en ce moment et puisque vous êtes d'accord, c'est plus Pythonic (quelque chose que je ne saurais pas car c'est mon premier projet Python), je suppose que je m'en tiens à cela. Cependant, il est vraiment bon de connaître le rappel. Merci encore.
Paolo Bergantino

1
Je vous remercie. Cette méthode fonctionne très bien avec modelformset_factory. Je ne pouvais pas faire fonctionner correctement les autres méthodes avec les modèles de formulaires, mais cette méthode était très simple.
Spike

Le curry fonctionnel crée essentiellement une fermeture, n'est-ce pas? Pourquoi dites-vous que la solution de @ mmarshall est plus pythonique? Btw, merci pour votre réponse. J'aime cette approche.
Josh

9

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 alors j'espère que cela aidera quelqu'un.

Une note sur l'utilisation de inlineformset_factory.

J'ai utilisé cette solution moi-même et cela a fonctionné parfaitement, jusqu'à ce que je l'ai essayé avec le inlineformset_factory. J'utilisais Django 1.0.2 et j'ai eu une étrange exception KeyError. J'ai mis à niveau vers le dernier coffre 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))

C'est la même chose modelformset_factory. Merci pour cette réponse!
thnee

9

À partir du commit e091c18f50266097f648efc7cac2503968e9d217 le Tue Aug 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 utilisant le résultat pour construire un objet de classe de son tapez à 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 curryed ou un partialobjet passé au lieu d'un formulaire "provoque le canard à vous mordre" pour ainsi dire: il appellera une fonction avec les paramètres de construction d'un ModelFormClassobjet, 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 n'importe quelle classe spécifiée comme premier paramètre, qui appelle ensuite super.__init__après avoir updateing les kwargs avec ceux fournis lors de l'appel de la fonction de 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 suit:

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 le moment
  • les paramètres fournis pourraient entrer en conflit et écraser ceux utilisés par le code qui utilisera l'objet retourné par le constructeur

Merci, semble très bien fonctionner dans Django 1.10.1 contrairement à certaines des autres solutions ici.
fpghost le

1
@fpghost gardez à l'esprit que, au moins jusqu'à 1.9 (je ne suis toujours pas sur 1.10 pour un certain nombre de raisons) si tout ce que vous avez à faire est de changer le QuerySet sur lequel le formulaire est construit, vous pouvez le mettre à jour sur le a renvoyé MyFormSet en modifiant son attribut .queryset avant de l'utiliser. Moins flexible que cette méthode, mais beaucoup plus simple à lire / comprendre.
RobM

3

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

À 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 formset. Je suis content, mais je ne sais pas pourquoi cela fonctionne - cela semble faux. Aucune suggestion?


Hmmm ... Maintenant, si j'essaie d'accéder à l'attribut form d'une instance de MyFormSet, il renvoie (correctement) <function _curried> au lieu de <MyForm>. Des suggestions sur la façon d'accéder au formulaire réel, cependant? J'ai essayé MyFormSet.form.Meta.model.
trubliphone

Oups ... Je dois appeler la fonction curry pour accéder au formulaire. MyFormSet.form().Meta.model. Évident vraiment.
trubliphone

J'ai essayé d'appliquer votre solution à mon problème, mais je pense que je ne comprends pas entièrement votre réponse. Des idées si votre approche peut être appliquée à mon problème ici? stackoverflow.com/questions/14176265/…
finspin

1

J'ai passé du temps à essayer de résoudre 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 déjà utilisée avec les formulaires modèles Django).

J'ai essayé la méthode curry () comme décrit ci-dessus, mais je ne pouvais tout simplement pas la faire fonctionner avec Django 1.0, donc à la fin je suis revenue à 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 dans une autre fonction. Je pense que le fait que cela me semble étrange est un blocage par rapport à mon expérience de programmation précédente et je pense que quelqu'un avec une formation dans des langages plus dynamiques ne ferait pas de cas!


1

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

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

sur la base de 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 lancez-le en vue comme

formset_factory(form=ServiceForm.make_service_form(affiliate))

6
Django 1.9 a rendu tout cela inutile, utilisez plutôt form_kwargs.
Paolo Bergantino

Dans mon travail actuel, nous devons utiliser l'héritage django 1.7 ((
alexey_efimov

0

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 formset au BaseFormSetlieu de form.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.