Django przekazuje niestandardowe parametry formularza do zestawu formularzy

150

Zostało to naprawione w Django 1.9 za pomocą form_kwargs .

Mam formularz Django, który wygląda następująco:

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)

Nazywam ten formularz czymś takim:

form = ServiceForm(affiliate=request.affiliate)

Gdzie request.affiliatejest zalogowany użytkownik. Działa to zgodnie z przeznaczeniem.

Mój problem polega na tym, że chcę teraz przekształcić tę pojedynczą formę w zestaw form. Nie mogę dowiedzieć się, w jaki sposób mogę przekazać informacje o podmiotach stowarzyszonych do poszczególnych formularzy podczas tworzenia zestawu formularzy. Zgodnie z dokumentacją, aby utworzyć z tego formularz, muszę zrobić coś takiego:

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

A potem muszę to stworzyć w ten sposób:

formset = ServiceFormSet()

Jak mogę przekazać w ten sposób affiliate = request.affiliate do poszczególnych formularzy?

Paolo Bergantino
źródło

Odpowiedzi:

105

Użyłbym functools.partial i 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)

Myślę, że jest to najczystsze podejście i nie wpływa w żaden sposób na ServiceForm (tj. Utrudniając tworzenie podklas).

Carl Meyer
źródło
Na mnie to nie działa. Otrzymuję błąd: AttributeError: Obiekt „_curriedFormSet” nie ma atrybutu „get”
Paolo Bergantino
Nie mogę powielić tego błędu. Jest to również dziwne, ponieważ zestaw formularzy zwykle nie ma atrybutu „get”, więc wygląda na to, że robisz coś dziwnego w swoim kodzie. (Ponadto zaktualizowałem odpowiedź, aby pozbyć się dziwactw, takich jak „_curriedFormSet”).
Carl Meyer,
Wracam do tego, ponieważ chciałbym, aby Twoje rozwiązanie działało. Mogę zadeklarować zestaw formularzy w porządku, ale jeśli spróbuję go wydrukować, robiąc {{formset}} otrzymuję błąd „nie ma atrybutu 'get'”. Dzieje się tak w przypadku dowolnego dostarczonego przez Ciebie rozwiązania. Jeśli przejdę pętlę przez zestaw formularzy i wydrukuję formularze jako {{formularz}}, ponownie otrzymam błąd. Jeśli zapętlę i wydrukuję na przykład jako {{form.as_table}}, otrzymam puste tabele formularzy, tj. żadne pola nie są drukowane. Jakieś pomysły?
Paolo Bergantino
Masz rację, przepraszam; moje wcześniejsze testy nie były wystarczające. Wyśledziłem to i psuje się z powodu pewnych dziwactw w wewnętrznym działaniu FormSets. Jest sposób na obejście tego problemu, ale zaczyna on tracić pierwotną elegancję ...
Carl Meyer
5
Jeśli wątek komentarza tutaj nie ma sensu, to dlatego, że właśnie zredagowałem odpowiedź, aby użyć Pythona functools.partialzamiast Django django.utils.functional.curry. Robią to samo, z wyjątkiem tego, że functools.partialzamiast zwykłej funkcji Pythona zwracają odrębny wywoływalny typ, a partialtyp nie wiąże się jako metoda instancji, co starannie rozwiązuje problem, który ten wątek komentarza był w dużej mierze poświęcony debugowaniu.
Carl Meyer,
81

Oficjalny sposób dokumentu

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
źródło
8
powinien to być właściwy sposób zrobienia tego teraz. zaakceptowana odpowiedź działa i jest fajna, ale to hack
Junchao Gu
zdecydowanie najlepsza odpowiedź i właściwy sposób, aby to zrobić.
yaniv14
Działa również w Django 1.11 docs.djangoproject.com/en/1.11/topics/forms/formsets/ ...
ruohola
46

Zbudowałbym klasę formularza dynamicznie w funkcji, tak aby miała dostęp do partnera poprzez zamknięcie:

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

Jako bonus, nie musisz przepisywać zestawu zapytań w polu opcji. Wadą jest to, że podklasy są trochę zabawne. (Każda podklasa musi być wykonana w podobny sposób).

edytować:

W odpowiedzi na komentarz możesz wywołać tę funkcję w dowolnym miejscu, w którym użyjesz nazwy klasy:

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
źródło
Dzięki - zadziałało. Odkładam oznaczenie tego jako zaakceptowanego, ponieważ mam nadzieję, że istnieje czystsza opcja, ponieważ robienie tego w ten sposób zdecydowanie wydaje się fajne.
Paolo Bergantino
Oznaczanie jako zaakceptowane, ponieważ najwyraźniej jest to najlepszy sposób na zrobienie tego. Czuje się dziwnie, ale załatwia sprawę. :) Dziękuję Ci.
Paolo Bergantino
Myślę, że Carl Meyer ma czystszy sposób, którego szukałeś.
Jarret Hardie
Używam tej metody z Django ModelForms.
chefsmart
Podoba mi się to rozwiązanie, ale nie jestem pewien, jak go użyć w widoku przypominającym zestaw formularzy. Czy masz jakieś dobre przykłady wykorzystania tego w widoku? Wszelkie sugestie są mile widziane.
Joe J
16

Oto, co zadziałało dla mnie, 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):

Mam nadzieję, że to komuś pomogło, wystarczająco długo zajęło mi to rozgryzienie;)

rix
źródło
1
Czy mógłbyś mi wyjaśnić, dlaczego staticmethodjest tu potrzebny?
fpghost
9

Podoba mi się rozwiązanie zamykające, które jest „czystsze” i bardziej Pythonic (więc +1 do odpowiedzi mmarshall), ale formularze Django mają również mechanizm wywołania zwrotnego, którego można użyć do filtrowania zestawów zapytań w zestawach formularzy.

Nie jest to również udokumentowane, co moim zdaniem jest wskaźnikiem, że twórcom Django może się to nie podobać.

Więc zasadniczo tworzysz zestaw formularzy tak samo, ale dodajesz wywołanie zwrotne:

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

To jest tworzenie instancji klasy, która wygląda następująco:

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

To powinno dać ci ogólny pomysł. Uczynienie wywołania zwrotnego metodą obiektową, taką jak ta, jest nieco bardziej skomplikowane, ale zapewnia nieco większą elastyczność w porównaniu z wykonywaniem prostego wywołania zwrotnego funkcji.

Van Gale
źródło
1
Dziękuję za Twoją odpowiedź. Używam teraz rozwiązania mmarshall i skoro zgadzasz się, że jest bardziej Pythonic (coś, czego nie wiedziałbym, ponieważ jest to mój pierwszy projekt w Pythonie), myślę, że się tego trzymam. Jednak zdecydowanie warto wiedzieć o oddzwonieniu. Dzięki jeszcze raz.
Paolo Bergantino
1
Dziękuję Ci. W ten sposób działa świetnie z modelformset_factory. Nie mogłem znaleźć innych sposobów poprawnej pracy z modelformsets, ale ten sposób był bardzo prosty.
Spike
Funkcjonalność curry zasadniczo tworzy zamknięcie, prawda? Dlaczego mówisz, że rozwiązanie @ mmarshall jest bardziej Pythonic? Przy okazji, dziękuję za odpowiedź. Podoba mi się to podejście.
Josh
9

Chciałem umieścić to jako komentarz do odpowiedzi Carla Meyersa, ale ponieważ wymaga to punktów, po prostu umieściłem go tutaj. Zajęło mi to 2 godziny, więc mam nadzieję, że to komuś pomoże.

Uwaga dotycząca korzystania z pliku inlineformset_factory.

Sam korzystałem z tego rozwiązania i działało idealnie, dopóki nie wypróbowałem go z inlineformset_factory. Używałem Django 1.0.2 i dostałem dziwny wyjątek KeyError. Zaktualizowałem do najnowszego bagażnika i działało bezpośrednio.

Teraz mogę go używać podobnie do tego:

BookFormSet = inlineformset_factory(Author, Book, form=BookForm)
BookFormSet.form = staticmethod(curry(BookForm, user=request.user))
Johan Berg Nilsson
źródło
To samo dotyczy modelformset_factory. Dzięki za tę odpowiedź!
czw
9

W dniu zatwierdzenia e091c18f50266097f648efc7cac2503968e9d217 we wtorek 14 sierpnia 23:44:46 2012 +0200 zaakceptowane rozwiązanie nie może już działać.

Bieżąca wersja funkcji django.forms.models.modelform_factory () używa "techniki konstrukcji typu", wywołując funkcję type () w przekazanym formularzu w celu pobrania typu metaklasy, a następnie używając wyniku do skonstruowania obiektu klasy swojego pisz w locie:

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

Oznacza to, że nawet curryed lub partialobiekt przekazany zamiast formularza "powoduje, że kaczka cię ugryzie", że tak powiem: wywoła funkcję z parametrami konstrukcji ModelFormClassobiektu, zwracając komunikat o błędzie:

function() argument 1 must be code, not str

Aby obejść ten problem, napisałem funkcję generatora, która używa zamknięcia do zwrócenia podklasy dowolnej klasy określonej jako pierwszy parametr, a następnie wywołuje super.__init__po updatekwargach te podane w wywołaniu funkcji generatora:

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

Następnie w swoim kodzie zadzwonisz do fabryki formularzy jako:

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

zastrzeżenia:

  • ten otrzymał bardzo niewiele testów, przynajmniej na razie
  • podane parametry mogą kolidować i nadpisywać te używane przez dowolny kod, który użyje obiektu zwróconego przez konstruktora
RobM
źródło
Dzięki, wygląda na to, że działa bardzo dobrze w Django 1.10.1 w przeciwieństwie do niektórych innych rozwiązań tutaj.
fpghost
1
@fpghost pamiętaj, że co najmniej do 1,9 (nadal nie jestem na 1.10 z wielu powodów), jeśli wszystko, co musisz zrobić, to zmienić zestaw QuerySet, na podstawie którego zbudowany jest formularz, możesz go zaktualizować na zwrócił MyFormSet, zmieniając jego atrybut .queryset przed jego użyciem. Mniej elastyczna niż ta metoda, ale znacznie prostsza do odczytania / zrozumienia.
RobM
3

Rozwiązanie Carla Meyera prezentuje się bardzo elegancko. Próbowałem go zaimplementować w modelformsets. Miałem wrażenie, że nie mogę wywołać metod statycznych w klasie, ale w niewytłumaczalny sposób działa:

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

Moim zdaniem, jeśli zrobię coś takiego:

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

Następnie słowo kluczowe „request” jest propagowane do wszystkich formularzy członkowskich mojego zestawu formularzy. Jestem zadowolony, ale nie mam pojęcia, dlaczego to działa - wydaje się źle. Jakieś sugestie?

trubliphone
źródło
Hmmm ... Teraz, jeśli spróbuję uzyskać dostęp do atrybutu formularza wystąpienia MyFormSet, to (poprawnie) zwraca <function _curried> zamiast <MyForm>. Jakieś sugestie, jak uzyskać dostęp do faktycznego formularza? Próbowałem MyFormSet.form.Meta.model.
trubliphone
Ups ... Muszę wywołać funkcję curried, aby uzyskać dostęp do formularza. MyFormSet.form().Meta.model. Naprawdę oczywiste.
trubliphone
Próbowałem zastosować twoje rozwiązanie do mojego problemu, ale myślę, że nie do końca rozumiem całą twoją odpowiedź. Jakieś pomysły, czy twoje podejście można zastosować tutaj do mojego problemu? stackoverflow.com/questions/14176265/ ...
finspin
1

Spędziłem trochę czasu próbując rozwiązać ten problem, zanim zobaczyłem ten post.

Rozwiązaniem, które wymyśliłem, było rozwiązanie zamykające (i jest to rozwiązanie, którego używałem wcześniej z formularzami modelowymi Django).

Wypróbowałem metodę curry (), jak opisano powyżej, ale po prostu nie mogłem zmusić jej do pracy z Django 1.0, więc w końcu wróciłem do metody zamknięcia.

Metoda zamykania jest bardzo zgrabna, a jedyną drobną dziwnością jest to, że definicja klasy jest zagnieżdżona w widoku lub innej funkcji. Myślę, że fakt, że wydaje mi się to dziwne, jest rozłączeniem z moim poprzednim doświadczeniem w programowaniu i myślę, że ktoś z doświadczeniem w bardziej dynamicznych językach nie mrugnąłby okiem!

Nick Craig-Wood
źródło
1

Musiałem zrobić podobną rzecz. Jest to podobne do curryrozwiązania:

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
źródło
1

na podstawie tej odpowiedzi znalazłem bardziej przejrzyste rozwiązanie:

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

I uruchom to tak, jak wygląda

formset_factory(form=ServiceForm.make_service_form(affiliate))
alexey_efimov
źródło
6
Django 1.9 sprawił, że wszystko to było niepotrzebne, zamiast tego użyj form_kwargs.
Paolo Bergantino
W mojej obecnej pracy musimy użyć starszego django 1.7 ((
alexey_efimov
0

Jestem tu nowicjuszem, więc nie mogę dodawać komentarzy. Mam nadzieję, że ten kod też zadziała:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

co do dodawania dodatkowych parametrów do BaseFormSetzestawu form zamiast formularza.

Philamer Sune
źródło