Django Passing Parameter Formulir Kustom ke Formset

150

Ini diperbaiki di Django 1.9 dengan form_kwargs .

Saya memiliki Formulir Django yang terlihat seperti ini:

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)

Saya menyebut formulir ini dengan sesuatu seperti ini:

form = ServiceForm(affiliate=request.affiliate)

Di mana request.affiliatepengguna yang masuk Ini berfungsi sebagaimana dimaksud.

Masalah saya adalah saya sekarang ingin mengubah bentuk tunggal ini menjadi sebuah formset. Apa yang saya tidak tahu adalah bagaimana saya bisa meneruskan informasi afiliasi ke formulir individu ketika membuat formset. Menurut dokumen untuk membuat format dari ini saya perlu melakukan sesuatu seperti ini:

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

Dan kemudian saya harus membuatnya seperti ini:

formset = ServiceFormSet()

Sekarang bagaimana saya bisa meneruskan afiliasi = request.affiliate ke formulir individual dengan cara ini?

Paolo Bergantino
sumber

Jawaban:

105

Saya akan menggunakan functools.partial dan 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)

Saya pikir ini adalah pendekatan terbersih, dan tidak mempengaruhi ServiceForm dengan cara apa pun (yaitu dengan membuatnya sulit untuk subkelas).

Carl Meyer
sumber
Itu tidak bekerja untuk saya. Saya mendapatkan kesalahan: AttributeError: objek '_curriedFormSet' tidak memiliki atribut 'get'
Paolo Bergantino
Saya tidak dapat menduplikasi kesalahan ini. Ini juga aneh karena sebuah formset biasanya tidak memiliki atribut 'get', jadi sepertinya Anda mungkin melakukan sesuatu yang aneh dalam kode Anda. (Juga, saya memperbarui jawabannya dengan cara untuk menghilangkan keanehan seperti '_curriedFormSet').
Carl Meyer
Saya meninjau kembali ini karena saya ingin solusi Anda berfungsi. Saya dapat mendeklarasikan formset baik-baik saja, tetapi jika saya mencoba mencetaknya melakukan {{formset}} adalah ketika saya mendapatkan kesalahan "tidak memiliki atribut 'dapatkan'". Itu terjadi dengan salah satu solusi yang Anda berikan. Jika saya mengulang formset dan mencetak form sebagai {{form}} saya mendapatkan kesalahan lagi. Jika saya mengulang dan mencetak sebagai {{form.as_table}} misalnya, saya mendapatkan tabel formulir kosong, yaitu. tidak ada bidang yang dicetak. Ada ide?
Paolo Bergantino
Anda benar, saya minta maaf; pengujian saya sebelumnya tidak cukup jauh. Saya melacak ini, dan rusak karena beberapa keanehan dalam cara FormSets bekerja secara internal. Ada cara untuk mengatasi masalah, tetapi mulai kehilangan keanggunan asli ...
Carl Meyer
5
Jika utas komentar di sini tidak masuk akal, itu karena saya baru saja mengedit jawaban untuk menggunakan Python, functools.partialbukan Django django.utils.functional.curry. Mereka melakukan hal yang sama, kecuali yang functools.partialmengembalikan tipe callable yang berbeda dan bukan fungsi Python biasa, dan partialtipe tersebut tidak mengikat sebagai metode instance, yang dengan rapi memecahkan masalah thread komentar ini sebagian besar dikhususkan untuk debugging.
Carl Meyer
81

Cara Dokumen Resmi

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

sergi0
sumber
8
ini seharusnya cara yang benar untuk melakukannya sekarang. jawaban yang diterima bekerja dan bagus tetapi merupakan peretasan
Junchao Gu
pasti jawaban terbaik dan cara yang benar untuk melakukannya.
yaniv14
Juga berfungsi di Django 1.11 docs.djangoproject.com/en/1.11/topics/forms/formsets/…
ruohola
46

Saya akan membangun kelas bentuk secara dinamis dalam suatu fungsi, sehingga memiliki akses ke afiliasi melalui penutupan:

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

Sebagai bonus, Anda tidak perlu menulis ulang kueryset di bidang opsi. The downside adalah bahwa subclassing sedikit funky. (Setiap subkelas harus dibuat dengan cara yang sama.)

edit:

Menanggapi komentar, Anda dapat memanggil fungsi ini tentang tempat yang akan Anda gunakan nama kelas:

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
sumber
Terima kasih - itu berhasil. Saya menunda menandai ini sebagai diterima karena saya agak berharap ada pilihan yang lebih bersih, karena melakukannya dengan cara ini pasti terasa funky.
Paolo Bergantino
Menandai telah diterima karena tampaknya ini adalah cara terbaik untuk melakukannya. Terasa aneh, tetapi berhasil. :) Terima kasih.
Paolo Bergantino
Carl Meyer, saya pikir, memiliki cara yang lebih bersih yang Anda cari.
Jarret Hardie
Saya menggunakan metode ini dengan Django ModelForms.
Chefsmart
Saya suka solusi ini, tapi saya tidak yakin bagaimana menggunakannya dalam tampilan seperti formset. Apakah Anda memiliki contoh bagus tentang cara menggunakan ini dalam tampilan? Setiap saran sangat dihargai.
Joe J
16

Inilah yang berhasil bagi saya, 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):

Semoga ini bisa membantu seseorang, butuh waktu lama bagi saya untuk mengetahuinya;)

rix
sumber
1
Bisakah Anda menjelaskan kepada saya mengapa staticmethoddiperlukan di sini?
fpghost
9

Saya suka solusi penutupan untuk menjadi "lebih bersih" dan lebih Pythonic (jadi +1 ke jawaban mmarshall) tetapi formulir Django juga memiliki mekanisme panggilan balik yang dapat Anda gunakan untuk memfilter queryset dalam format.

Ini juga tidak didokumentasikan, yang saya pikir merupakan indikator yang mungkin tidak disukai oleh para Django.

Jadi pada dasarnya Anda membuat formset Anda sama tetapi menambahkan callback:

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

Ini membuat instance kelas yang terlihat seperti ini:

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

Ini seharusnya memberi Anda ide umum. Ini sedikit lebih kompleks membuat callback menjadi metode objek seperti ini, tetapi memberi Anda sedikit lebih banyak fleksibilitas daripada melakukan fungsi callback sederhana.

Van Gale
sumber
1
Terima kasih atas jawabannya. Saya menggunakan solusi mmarshall sekarang dan karena Anda setuju itu lebih Pythonic (sesuatu yang saya tidak akan tahu karena ini adalah proyek Python pertama saya) Saya kira saya tetap dengan itu. Pasti bagus untuk mengetahui tentang panggilan balik itu. Terima kasih lagi.
Paolo Bergantino
1
Terima kasih. Cara ini berfungsi baik dengan modelformset_factory. Saya tidak bisa mendapatkan cara lain bekerja dengan modelforms dengan benar tetapi cara ini sangat mudah.
Spike
Fungsi kari pada dasarnya menciptakan penutupan, bukan? Mengapa Anda mengatakan bahwa solusi @ mmarshall lebih Pythonic? Btw, terima kasih atas jawaban Anda. Saya suka pendekatan ini.
Josh
9

Saya ingin menempatkan ini sebagai komentar untuk jawaban Carl Meyers, tetapi karena itu memerlukan poin saya hanya menempatkannya di sini. Ini membutuhkan waktu 2 jam untuk mencari tahu jadi saya harap ini akan membantu seseorang.

Catatan tentang penggunaan inlineformset_factory.

Saya menggunakan solusi itu sendiri dan bekerja dengan sempurna, sampai saya mencobanya dengan inlineformset_factory. Saya menjalankan Django 1.0.2 dan mendapatkan beberapa pengecualian KeyError yang aneh. Saya memutakhirkan ke bagasi terbaru dan langsung bekerja.

Sekarang saya dapat menggunakannya mirip dengan ini:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
Johan Berg Nilsson
sumber
Hal yang sama berlaku untuk modelformset_factory. Terima kasih atas jawaban ini!
Lutut
9

Pada komit e091c18f50266097f648efc7cac2503968e9d217 pada Selasa 14 Agustus 23:44:46 2012 +200 solusi yang diterima tidak dapat bekerja lagi.

Versi saat ini dari fungsi django.forms.models.modelform_factory () menggunakan "teknik konstruksi tipe", memanggil fungsi type () pada formulir yang dilewati untuk mendapatkan tipe metaclass, kemudian menggunakan hasilnya untuk membangun objek kelas dari objeknya. ketik dengan cepat ::

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

Ini berarti bahkan curryed atau partialobjek yang dilewatkan sebagai ganti bentuk "menyebabkan bebek menggigit Anda" sehingga berbicara: itu akan memanggil fungsi dengan parameter konstruksi ModelFormClassobjek, mengembalikan pesan kesalahan ::

function() argument 1 must be code, not str

Untuk mengatasi ini, saya menulis fungsi generator yang menggunakan penutupan untuk mengembalikan subkelas dari kelas yang ditentukan sebagai parameter pertama, yang kemudian memanggil super.__init__setelah updatekwarg dengan yang disediakan pada panggilan fungsi generator ::

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

Kemudian dalam kode Anda, Anda akan memanggil form factory sebagai ::

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

peringatan:

  • ini menerima sangat sedikit pengujian, setidaknya untuk saat ini
  • parameter yang disediakan dapat berbenturan dan menimpa yang digunakan oleh kode apa pun yang akan menggunakan objek yang dikembalikan oleh konstruktor
RobM
sumber
Terima kasih, tampaknya berfungsi sangat baik di Django 1.10.1 tidak seperti beberapa solusi lain di sini.
fpghost
1
@ fpghost perlu diingat bahwa, setidaknya hingga 1,9 (saya masih tidak pada 1,10 karena sejumlah alasan) jika semua yang perlu Anda lakukan adalah mengubah QuerySet yang menjadi dasar formulir ini dibangun, Anda dapat memperbaruinya di mengembalikan MyFormSet dengan mengubah atribut .queryset sebelum menggunakannya. Kurang fleksibel dibandingkan metode ini, tetapi lebih mudah dibaca / dimengerti.
RobM
3

Solusi Carl Meyer terlihat sangat elegan. Saya mencoba menerapkannya untuk modelformsets. Saya mendapat kesan bahwa saya tidak bisa memanggil metode statis dalam suatu kelas, tetapi hal berikut ini bisa dijelaskan:

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

Dalam pandangan saya, jika saya melakukan sesuatu seperti ini:

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

Kemudian kata kunci "permintaan" akan disebarkan ke semua bentuk anggota dari rangkaian saya. Saya senang, tetapi saya tidak tahu mengapa ini bekerja - sepertinya salah. Ada saran?

trubliphone
sumber
Hmmm ... Sekarang jika saya mencoba mengakses atribut form dari instance MyFormSet, ia (dengan benar) mengembalikan <function _curried> alih-alih <MyForm>. Adakah saran tentang cara mengakses formulir yang sebenarnya? Saya sudah mencoba MyFormSet.form.Meta.model.
trubliphone
Aduh ... Saya harus memanggil fungsi kari untuk mengakses formulir. MyFormSet.form().Meta.model. Jelas sekali.
trubliphone
Saya sudah mencoba menerapkan solusi Anda untuk masalah saya, tetapi saya rasa saya tidak sepenuhnya memahami seluruh jawaban Anda. Adakah ide jika pendekatan Anda dapat diterapkan pada masalah saya di sini? stackoverflow.com/questions/14176265/…
finspin
1

Saya menghabiskan beberapa waktu untuk mencari tahu masalah ini sebelum saya melihat posting ini.

Solusi yang saya temukan adalah solusi penutupan (dan ini adalah solusi yang pernah saya gunakan sebelumnya dengan bentuk model Django).

Saya mencoba metode curry () seperti yang dijelaskan di atas, tetapi saya tidak bisa membuatnya bekerja dengan Django 1.0 jadi pada akhirnya saya kembali ke metode penutupan.

Metode penutupan sangat rapi dan hanya sedikit keanehan adalah bahwa definisi kelas bersarang di dalam tampilan atau fungsi lain. Saya pikir fakta bahwa ini terlihat aneh bagi saya adalah hangup dari pengalaman pemrograman saya sebelumnya dan saya pikir seseorang dengan latar belakang dalam bahasa yang lebih dinamis tidak akan kelopak mata!

Nick Craig-Wood
sumber
1

Saya harus melakukan hal serupa. Ini mirip dengan currysolusinya:

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
sumber
1

berdasarkan jawaban ini saya menemukan solusi yang lebih jelas:

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

Dan jalankan dalam tampilan seperti

formset_factory(form=ServiceForm.make_service_form(affiliate))
alexey_efimov
sumber
6
Django 1.9 membuat semua ini tidak perlu, gunakan form_kwargs sebagai gantinya.
Paolo Bergantino
Dalam pekerjaan saya saat ini, kita perlu menggunakan legacy django 1.7 ((
alexey_efimov
0

Saya seorang pemula di sini jadi saya tidak bisa menambahkan komentar. Saya harap kode ini juga berfungsi:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

seperti untuk menambahkan parameter tambahan ke formset, BaseFormSetbukan form.

Philamer Sune
sumber