Django Übergeben benutzerdefinierter Formularparameter an Formset

150

Dies wurde in Django 1.9 mit form_kwargs behoben .

Ich habe ein Django-Formular, das so aussieht:

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)

Ich nenne dieses Formular so etwas:

form = ServiceForm(affiliate=request.affiliate)

Wo request.affiliateist der angemeldete Benutzer? Dies funktioniert wie vorgesehen.

Mein Problem ist, dass ich dieses einzelne Formular jetzt in ein Formularset verwandeln möchte. Was ich nicht herausfinden kann, ist, wie ich die Partnerinformationen beim Erstellen des Formularsatzes an die einzelnen Formulare weitergeben kann. Laut den Dokumenten muss ich so etwas tun, um daraus ein Formset zu machen:

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

Und dann muss ich es so erstellen:

formset = ServiceFormSet()

Wie kann ich nun Affiliate = request.affiliate auf diese Weise an die einzelnen Formulare übergeben?

Paolo Bergantino
quelle

Antworten:

105

Ich würde functools.partial und functools.wraps verwenden :

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

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

Ich denke, dies ist der sauberste Ansatz und wirkt sich in keiner Weise auf ServiceForm aus (dh indem es die Unterklasse erschwert).

Carl Meyer
quelle
Es funktioniert nicht für mich. Ich erhalte die Fehlermeldung: AttributeError: Das Objekt '_curriedFormSet' hat kein Attribut 'get'
Paolo Bergantino
Ich kann diesen Fehler nicht duplizieren. Es ist auch seltsam, weil ein Formset normalerweise kein 'get'-Attribut hat. Es scheint also, dass Sie etwas Seltsames in Ihrem Code tun. (Außerdem habe ich die Antwort aktualisiert, um Kuriositäten wie '_curriedFormSet' zu beseitigen.)
Carl Meyer
Ich besuche dies noch einmal, weil ich möchte, dass Ihre Lösung funktioniert. Ich kann das Formset als gut deklarieren, aber wenn ich versuche, es mit {{formset}} zu drucken, erhalte ich den Fehler "hat kein Attribut 'get'". Dies geschieht mit beiden von Ihnen bereitgestellten Lösungen. Wenn ich das Formularset durchlaufe und die Formulare als {{form}} drucke, wird der Fehler erneut angezeigt. Wenn ich zum Beispiel als {{form.as_table}} schleife und drucke, erhalte ich leere Formulartabellen, d. H. Es werden keine Felder gedruckt. Irgendwelche Ideen?
Paolo Bergantino
Du hast recht, es tut mir leid; Meine früheren Tests gingen nicht weit genug. Ich habe dies aufgespürt und es bricht aufgrund einiger Kuriositäten in der internen Funktionsweise von FormSets ab. Es gibt eine Möglichkeit, das Problem zu umgehen, aber es beginnt, die ursprüngliche Eleganz zu verlieren ...
Carl Meyer
5
Wenn der Kommentarthread hier keinen Sinn ergibt, habe ich gerade die Antwort bearbeitet, um Pythons functools.partialanstelle von Djangos zu verwenden django.utils.functional.curry. Sie tun dasselbe, außer dass functools.partialanstelle einer regulären Python-Funktion ein bestimmter aufrufbarer Typ zurückgegeben wird und der partialTyp nicht als Instanzmethode gebunden wird, wodurch das Problem, dass dieser Kommentarthread hauptsächlich dem Debuggen gewidmet war, sauber gelöst wird.
Carl Meyer
81

Offizieller Dokumentweg

Django 2.0:

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

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

sergi0
quelle
8
Dies sollte jetzt die richtige Vorgehensweise sein. Die akzeptierte Antwort funktioniert und ist nett, aber ein Hack
Junchao Gu
definitiv die beste Antwort und der richtige Weg, es zu tun.
yaniv14
Funktioniert auch in Django 1.11 docs.djangoproject.com/de/1.11/topics/forms/formsets/…
ruohola
46

Ich würde die Formularklasse dynamisch in einer Funktion erstellen, damit sie über das Schließen Zugriff auf den Partner hat:

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

Als Bonus müssen Sie das Abfrageset im Optionsfeld nicht neu schreiben. Der Nachteil ist, dass Unterklassen ein wenig funky sind. (Jede Unterklasse muss auf ähnliche Weise erstellt werden.)

bearbeiten:

Als Antwort auf einen Kommentar können Sie diese Funktion an jedem Ort aufrufen, an dem Sie den Klassennamen verwenden würden:

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)
    ...
Matthew Marshall
quelle
Danke - das hat funktioniert. Ich halte mich zurück, um dies als akzeptiert zu markieren, weil ich hoffe, dass es eine sauberere Option gibt, da es sich auf jeden Fall funky anfühlt, wenn ich es so mache.
Paolo Bergantino
Als akzeptiert markieren, da dies anscheinend der beste Weg ist, dies zu tun. Fühlt sich komisch an, macht aber den Trick. :) Danke.
Paolo Bergantino
Carl Meyer hat, glaube ich, die sauberere Art, wie Sie gesucht haben.
Jarret Hardie
Ich verwende diese Methode mit Django ModelForms.
Chefkoch
Ich mag diese Lösung, bin mir aber nicht sicher, wie ich sie in einer Ansicht wie einem Formset verwenden soll. Haben Sie gute Beispiele, wie Sie dies in einer Ansicht verwenden können? Anregungen sind willkommen.
Joe J
16

Das hat bei mir funktioniert, 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):

Hoffe es hilft jemandem, ich habe lange genug gebraucht, um es herauszufinden;)

rix
quelle
1
Können Sie mir erklären, warum staticmethodhier gebraucht wird?
fpghost
9

Ich mag die Verschlusslösung, um "sauberer" und pythonischer zu sein (also +1 bis mmarshall Antwort), aber Django-Formulare haben auch einen Rückrufmechanismus, den Sie zum Filtern von Abfragesätzen in Formsätzen verwenden können.

Es ist auch nicht dokumentiert, was meiner Meinung nach ein Indikator ist, den die Django-Entwickler vielleicht nicht so sehr mögen.

Sie erstellen Ihr Formset also im Grunde gleich, fügen aber den Rückruf hinzu:

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

Dadurch wird eine Instanz einer Klasse erstellt, die folgendermaßen aussieht:

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

Dies sollte Ihnen die allgemeine Idee geben. Es ist etwas komplexer, den Rückruf zu einer Objektmethode wie dieser zu machen, bietet Ihnen jedoch etwas mehr Flexibilität als ein einfacher Funktionsrückruf.

Van Gale
quelle
1
Danke für die Antwort. Ich verwende gerade die Lösung von mmarshall und da Sie zustimmen, dass es mehr Pythonic ist (etwas, das ich nicht wissen würde, da dies mein erstes Python-Projekt ist), halte ich mich wohl daran. Es ist jedoch auf jeden Fall gut, über den Rückruf Bescheid zu wissen. Danke noch einmal.
Paolo Bergantino
1
Danke dir. Diese Methode funktioniert hervorragend mit modelformset_factory. Ich konnte die anderen Möglichkeiten, mit Modellformsätzen richtig zu arbeiten, nicht richtig verstehen, aber diese Methode war sehr einfach.
Spike
Die Curry-Funktion schafft im Wesentlichen einen Verschluss, nicht wahr? Warum sagen Sie, dass die Lösung von @ mmarshall eher pythonisch ist? Übrigens, danke für deine Antwort. Ich mag diesen Ansatz.
Josh
9

Ich wollte dies als Kommentar zu Carl Meyers Antwort platzieren, aber da dies Punkte erfordert, habe ich es einfach hier platziert. Ich habe 2 Stunden gebraucht, um das herauszufinden, also hoffe ich, dass es jemandem hilft.

Ein Hinweis zur Verwendung der inlineformset_factory.

Ich habe diese Lösung selbst verwendet und sie hat perfekt funktioniert, bis ich sie mit der inlineformset_factory ausprobiert habe. Ich habe Django 1.0.2 ausgeführt und eine seltsame KeyError-Ausnahme erhalten. Ich habe auf den neuesten Kofferraum aktualisiert und es hat direkt funktioniert.

Ich kann es jetzt ähnlich verwenden:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
Johan Berg Nilsson
quelle
Gleiches gilt für modelformset_factory. Danke für diese Antwort!
Knie
9

Ab Commit e091c18f50266097f648efc7cac2503968e9d217 am Di Aug 14 23:44:46 2012 +0200 kann die akzeptierte Lösung nicht mehr funktionieren.

Die aktuelle Version der Funktion django.forms.models.modelform_factory () verwendet eine "Typkonstruktionstechnik", die die Funktion type () für das übergebene Formular aufruft, um den Metaklassentyp abzurufen, und dann das Ergebnis verwendet, um ein Klassenobjekt von ihm zu erstellen Tippe on the fly ::

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

Dies bedeutet, dass sogar ein curryEd oder partialObjekt, das anstelle eines Formulars übergeben wurde, sozusagen "die Ente dazu bringt, Sie zu beißen": Es ruft eine Funktion mit den Konstruktionsparametern eines ModelFormClassObjekts auf und gibt die Fehlermeldung zurück ::

function() argument 1 must be code, not str

Um dies zu umgehen, habe ich eine Generatorfunktion geschrieben, die einen Abschluss verwendet, um eine Unterklasse einer als ersten Parameter angegebenen Klasse zurückzugeben, die dann super.__init__nach updatedem kwargs mit denjenigen aufgerufen wird, die beim Aufruf der Generatorfunktion angegeben wurden ::

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

Dann rufen Sie in Ihrem Code die Formularfactory als :: auf

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

Vorbehalte:

  • Dies wurde zumindest vorerst nur sehr wenig getestet
  • Die angegebenen Parameter können die von jedem Code verwendeten Parameter überschreiben und überschreiben, der das vom Konstruktor zurückgegebene Objekt verwendet
RobM
quelle
Danke, scheint in Django 1.10.1 im Gegensatz zu einigen anderen Lösungen hier sehr gut zu funktionieren.
fpghost
1
@fpghost Denken Sie daran, dass Sie mindestens bis zu 1.9 (ich bin aus mehreren Gründen immer noch nicht auf 1.10), wenn Sie nur das QuerySet ändern müssen, auf dem das Formular aufgebaut ist, es auf dem aktualisieren können hat MyFormSet zurückgegeben, indem das Attribut .queryset vor der Verwendung geändert wurde. Weniger flexibel als diese Methode, aber viel einfacher zu lesen / verstehen.
RobM
3

Die Lösung von Carl Meyer sieht sehr elegant aus. Ich habe versucht, es für modelformsets zu implementieren. Ich hatte den Eindruck, dass ich keine statischen Methoden innerhalb einer Klasse aufrufen konnte, aber Folgendes funktioniert unerklärlicherweise:

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

Meiner Ansicht nach, wenn ich so etwas mache:

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

Dann wird das Schlüsselwort "request" an alle Mitgliedsformulare meines Formularsatzes weitergegeben. Ich bin zufrieden, aber ich habe keine Ahnung, warum das funktioniert - es scheint falsch. Irgendwelche Vorschläge?

Trubliphon
quelle
Hmmm ... Wenn ich nun versuche, auf das Formularattribut einer Instanz von MyFormSet zuzugreifen, wird (korrekt) <function _curried> anstelle von <MyForm> zurückgegeben. Irgendwelche Vorschläge, wie Sie auf das eigentliche Formular zugreifen können? Ich habe es versucht MyFormSet.form.Meta.model.
Trubliphone
Whoops ... Ich muss die Curry-Funktion aufrufen, um auf das Formular zugreifen zu können. MyFormSet.form().Meta.model. Wirklich offensichtlich.
Trubliphone
Ich habe versucht, Ihre Lösung auf mein Problem anzuwenden, aber ich glaube, ich verstehe Ihre gesamte Antwort nicht vollständig. Irgendwelche Ideen, ob Ihr Ansatz hier auf mein Problem angewendet werden kann? stackoverflow.com/questions/14176265/…
Finspin
1

Ich habe einige Zeit damit verbracht, dieses Problem herauszufinden, bevor ich diesen Beitrag gesehen habe.

Die Lösung, die ich gefunden habe, war die Verschlusslösung (und diese Lösung habe ich zuvor mit Django-Modellformularen verwendet).

Ich habe die oben beschriebene curry () -Methode ausprobiert, konnte sie jedoch mit Django 1.0 nicht zum Laufen bringen, sodass ich am Ende zur Closure-Methode zurückkehrte.

Die Schließmethode ist sehr ordentlich und die einzige kleine Seltsamkeit besteht darin, dass die Klassendefinition in der Ansicht oder einer anderen Funktion verschachtelt ist. Ich denke, die Tatsache, dass dies für mich seltsam aussieht, ist ein Hangup aus meiner vorherigen Programmiererfahrung und ich denke, jemand mit einem Hintergrund in dynamischeren Sprachen würde kein Augenlid schlagen!

Nick Craig-Wood
quelle
1

Ich musste etwas Ähnliches tun. Dies ähnelt der curryLösung:

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), ... )
Rory
quelle
1

Aufgrund dieser Antwort fand ich eine klarere Lösung:

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

Und laufen Sie es in Sichtweite wie

formset_factory(form=ServiceForm.make_service_form(affiliate))
alexey_efimov
quelle
6
Django 1.9 hat dies unnötig gemacht. Verwenden Sie stattdessen form_kwargs.
Paolo Bergantino
In meiner aktuellen Arbeit müssen wir Legacy-Django 1.7 verwenden ((
alexey_efimov
0

Ich bin ein Neuling hier, daher kann ich keinen Kommentar hinzufügen. Ich hoffe, dass dieser Code auch funktioniert:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

zum Hinzufügen zusätzlicher Parameter zu den Formularsätzen BaseFormSetanstelle von Formularen.

Philamer Sune
quelle