Django Passando i parametri del modulo personalizzato al formset


150

Questo problema è stato risolto in Django 1.9 con form_kwargs .

Ho un Django Form che assomiglia a questo:

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)

Chiamo questo modulo con qualcosa del genere:

form = ServiceForm(affiliate=request.affiliate)

Dove request.affiliate l'utente che ha effettuato l'accesso. Funziona come previsto.

Il mio problema è che ora voglio trasformare questo singolo modulo in un formset. Quello che non riesco a capire è come posso trasmettere le informazioni di affiliazione ai singoli moduli durante la creazione del formset. Secondo i documenti per fare un formset da questo ho bisogno di fare qualcosa del genere:

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

E poi ho bisogno di crearlo in questo modo:

formset = ServiceFormSet()

Ora come posso passare affiliate = request.affiliate ai singoli moduli in questo modo?

Risposte:


105

Vorrei usare functools.partial e 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)

Penso che questo sia l'approccio più pulito e non influisca in alcun modo su ServiceForm (ovvero rendendo difficile la sottoclasse).


Non funziona per me. Ottengo l'errore: AttributeError: l'oggetto '_curriedFormSet' non ha attributo 'get'
Paolo Bergantino

Non riesco a duplicare questo errore. È anche strano perché un formset di solito non ha un attributo 'get', quindi sembra che potresti fare qualcosa di strano nel tuo codice. (Inoltre, ho aggiornato la risposta con un modo per sbarazzarsi di stranezze come '_curriedFormSet').
Carl Meyer,

Lo sto rivisitando perché mi piacerebbe che la tua soluzione funzionasse. Posso dichiarare bene il formset, ma se provo a stamparlo facendo {{formset}} è quando ottengo l'errore "non ha alcun attributo 'get'". Succede con entrambe le soluzioni fornite. Se eseguo il ciclo attraverso il formset e stampo i moduli come {{form}} ricevo di nuovo l'errore. Se ad esempio eseguo il ciclo e stampo come {{form.as_table}}, ottengo tabelle di moduli vuote, ad es. nessun campo viene stampato. Qualche idea?
Paolo Bergantino,

Hai ragione, mi dispiace; i miei test precedenti non sono andati abbastanza lontano. L'ho rintracciato e si rompe a causa di alcune stranezze nel modo in cui i FormSet funzionano internamente. C'è un modo per aggirare il problema, ma inizia a perdere l'eleganza originale ...
Carl Meyer

5
Se il thread dei commenti qui non ha senso, è perché ho appena modificato la risposta per usare Python functools.partialinvece di Django django.utils.functional.curry. Fanno la stessa cosa, tranne per il fatto che functools.partialrestituisce un tipo callable distinto invece di una normale funzione Python, e il partialtipo non si lega come un metodo di istanza, il che risolve ordinatamente il problema che questo thread di commento era ampiamente dedicato al debug.
Carl Meyer,

81

Modo documento ufficiale

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
questo dovrebbe essere il modo corretto di farlo ora. la risposta accettata funziona ed è carina ma è un trucco
Junchao Gu,

sicuramente la risposta migliore e il modo corretto di farlo.
yaniv14


46

Costruirei la classe del modulo in modo dinamico in una funzione, in modo che abbia accesso all'affiliato tramite chiusura:

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

Come bonus, non è necessario riscrivere il queryset nel campo delle opzioni. Il rovescio della medaglia è che la sottoclasse è un po 'funky. (Qualsiasi sottoclasse deve essere creata in modo simile.)

modificare:

In risposta a un commento, puoi chiamare questa funzione in qualsiasi luogo in cui utilizzeresti il ​​nome della 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)
    ...

Grazie - ha funzionato. Mi sto trattenendo dal contrassegnarlo come accettato perché spero che ci sia un'opzione più pulita, perché farlo in questo modo sembra decisamente funky.
Paolo Bergantino,

Contrassegnare come accettato poiché apparentemente questo è il modo migliore per farlo. Sembra strano, ma fa il trucco. :) Grazie.
Paolo Bergantino,

Carl Meyer ha, credo, il modo più pulito che stavi cercando.
Jarret Hardie

Sto usando questo metodo con Django ModelForms.
chefsmart,

Mi piace questa soluzione, ma non sono sicuro di come usarla in una vista come un formset. Hai qualche buon esempio di come usarlo in una vista? Ogni suggerimento è apprezzato.
Joe J,

16

Questo è ciò che ha funzionato per me, 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):

Spero che aiuti qualcuno, mi ha impiegato abbastanza tempo per capirlo;)


1
Potresti spiegarmi perché staticmethodè necessario qui?
fpghost,

9

Mi piace la soluzione di chiusura per essere "più pulito" e più Pythonic (quindi da +1 a mmarshall answer) ma i moduli Django hanno anche un meccanismo di callback che puoi usare per filtrare i queryset nei form.

Inoltre, non è documentato, che penso sia un indicatore che gli sviluppatori di Django potrebbero non apprezzare tanto.

Quindi fondamentalmente crei lo stesso formset ma aggiungi il callback:

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

Questo sta creando un'istanza di una classe che assomiglia a questa:

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

Questo dovrebbe darti l'idea generale. È un po 'più complesso rendere il callback un metodo oggetto come questo, ma ti dà un po' più di flessibilità rispetto a fare un semplice callback di funzione.


1
Grazie per la risposta. Sto usando la soluzione di mmarshall in questo momento e dato che sei d'accordo che è più Pythonic (qualcosa che non saprei dato che questo è il mio primo progetto Python), credo di essere fedele a quello. Tuttavia, è sicuramente utile conoscere il callback. Grazie ancora.
Paolo Bergantino,

1
Grazie. In questo modo funziona alla grande con modelformset_factory. Non riuscivo a far funzionare gli altri modi con i modelformset correttamente, ma in questo modo era molto semplice.
Spike

Il curry funzionale crea essenzialmente una chiusura, vero? Perché dici che la soluzione di @mmarshall è più Pythonic? A proposito, grazie per la tua risposta. Mi piace questo approccio.
Josh,

9

Volevo inserire questo come commento alla risposta di Carl Meyers, ma poiché ciò richiede punti, l'ho appena inserito qui. Mi ci sono volute 2 ore per capire, quindi spero che possa aiutare qualcuno.

Una nota sull'uso di inlineformset_factory.

Ho usato quella soluzione da solo e ha funzionato perfettamente, fino a quando non l'ho provata con inlineformset_factory. Stavo eseguendo Django 1.0.2 e ho avuto una strana eccezione KeyError. Ho aggiornato all'ultimo trunk e ha funzionato direttamente.

Ora posso usarlo in questo modo:

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

La stessa cosa vale modelformset_factory. Grazie per questa risposta!
giovedì

9

A partire da commit e091c18f50266097f648efc7cac2503968e9d217 il mar 14 ago 23:44:46 2012 +0200 la soluzione accettata non può più funzionare.

La versione corrente della funzione django.forms.models.modelform_factory () utilizza una "tecnica di costruzione del tipo", chiamando la funzione type () sul modulo passato per ottenere il tipo di metaclasse, quindi usando il risultato per costruire un oggetto di classe del suo digitare al volo ::

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

Questo significa che anche un curryed o un partialoggetto passato anziché un modulo "fa mordere l'anatra", per così dire: chiamerà una funzione con i parametri di costruzione di un ModelFormClassoggetto, restituendo il messaggio di errore:

function() argument 1 must be code, not str

Per ovviare a questo ho scritto una funzione generatore che utilizza una chiusura per restituire una sottoclasse di qualsiasi classe specificata come primo parametro, che quindi chiama super.__init__dopo aver updateing kwargs con quelli forniti nella chiamata della funzione generatore:

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

Quindi nel tuo codice chiamerai il form factory come ::

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

avvertimenti:

  • questo ha ricevuto pochissimi test, almeno per ora
  • i parametri forniti potrebbero scontrarsi e sovrascrivere quelli usati da qualunque codice utilizzerà l'oggetto restituito dal costruttore

Grazie, sembra funzionare molto bene in Django 1.10.1 a differenza di alcune delle altre soluzioni qui.
fpghost,

1
@fpghost tieni presente che, almeno fino all'1.9 (non sono ancora sull'1.10 per una serie di motivi) se tutto ciò che devi fare è cambiare il QuerySet su cui è costruito il modulo, puoi aggiornarlo sul ha restituito MyFormSet modificando il suo attributo .queryset prima di usarlo. Meno flessibile di questo metodo, ma molto più semplice da leggere / comprendere.
RobM

3

La soluzione di Carl Meyer sembra molto elegante. Ho provato a implementarlo per i modelformset. Avevo l'impressione di non poter chiamare metodi statici all'interno di una classe, ma il seguente funziona inspiegabilmente:

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

Dal mio punto di vista, se faccio qualcosa del genere:

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

Quindi la parola chiave "richiesta" viene propagata a tutte le forme membro del mio formset. Sono contento, ma non ho idea del perché funzioni, sembra sbagliato. Eventuali suggerimenti?


Hmmm ... Ora se provo ad accedere all'attributo del modulo di un'istanza di MyFormSet esso (correttamente) restituisce <funzione _curried> invece di <MyForm>. Qualche suggerimento su come accedere al modulo attuale, però? Ho provato MyFormSet.form.Meta.model.
trubliphone

Whoops ... Devo chiamare la funzione curry per accedere al modulo. MyFormSet.form().Meta.model. Ovvio davvero.
trubliphone

Ho cercato di applicare la tua soluzione al mio problema, ma penso di non comprendere appieno la tua intera risposta. Qualche idea se il tuo approccio può essere applicato al mio problema qui? stackoverflow.com/questions/14176265/...
finspin

1

Ho trascorso un po 'di tempo a cercare di capire questo problema prima di vedere questo post.

La soluzione che mi è venuta in mente è stata la soluzione di chiusura (ed è una soluzione che ho usato prima con i moduli del modello Django).

Ho provato il metodo curry () come descritto sopra, ma non sono riuscito a farlo funzionare con Django 1.0, quindi alla fine sono tornato al metodo di chiusura.

Il metodo di chiusura è molto accurato e l'unica leggera stranezza è che la definizione della classe è nidificata all'interno della vista o in un'altra funzione. Penso che il fatto che questo mi sembri strano sia un problema con la mia precedente esperienza di programmazione e penso che qualcuno con un background in linguaggi più dinamici non batterebbe le palpebre!


1

Ho dovuto fare una cosa simile. Questo è simile alla currysoluzione:

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

sulla base di questa risposta ho trovato una soluzione più chiara:

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

Ed eseguirlo in vista come

formset_factory(form=ServiceForm.make_service_form(affiliate))

6
Django 1.9 ha reso inutile tutto ciò, usa invece form_kwargs.
Paolo Bergantino,

Nel mio lavoro attuale dobbiamo usare legacy django 1.7 ((
alexey_efimov

0

Sono un principiante qui, quindi non posso aggiungere commenti. Spero che anche questo codice funzioni:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

come per l'aggiunta di parametri aggiuntivi al formset BaseFormSetinvece che al form.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.