Django Передача спеціальних параметрів форми до набору форм


150

Це було виправлено у Django 1.9 за допомогою form_kwargs .

У мене форма Django виглядає так:

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)

Я називаю цю форму приблизно так:

form = ServiceForm(affiliate=request.affiliate)

Де request.affiliateзареєстрований користувач. Це працює за призначенням.

Моя проблема полягає в тому, що я зараз хочу перетворити цю єдину форму в набір форматів. Що я не можу зрозуміти - це те, як я можу передавати інформацію про афілійованість до окремих форм під час створення набору форматів. Згідно з документами, щоб зробити набір форматів із цього, мені потрібно зробити щось подібне:

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

І тоді мені потрібно створити його так:

formset = ServiceFormSet()

Тепер, як я можу таким чином передати affiliate = request.affiliate до окремих форм?

Відповіді:


105

Я б використовував functools.partial і 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)

Я думаю, що це найчистіший підхід і жодним чином не впливає на ServiceForm (тобто, ускладнюючи підклас).


Це не працює для мене. Я отримую помилку: AttributeError: '_curriedFormSet' об’єкт не має атрибута 'get'
Paolo Bergantino

Я не можу дублювати цю помилку. Це також дивно, тому що набір форм зазвичай не має атрибута "get", тому, здається, ви можете робити щось дивне у своєму коді. (Також я оновив відповідь способом позбавлення від диваків, таких як "_curriedFormSet").
Карл Мейєр

Я переглядаю це, тому що я хотів би, щоб ваше рішення працювало. Я можу задекларувати набір форматів штрафом, але якщо я спробую роздрукувати його, виконуючи {{formset}} - це коли я отримую помилку "не має атрибута" отримати "". Це відбувається з будь-яким запропонованим вами рішенням. Якщо я проходжу через набір форматів і надрукую форми як {{form}}, я знову отримаю помилку. Якщо я циклічно і друкую як {{form.as_table}}, наприклад, я отримую порожні таблиці форм, тобто. жодні поля не друкуються. Будь-які ідеї?
Паоло Бергантіно

Ти маєш рацію, пробач; моє попереднє тестування не пройшло досить далеко. Я відслідковував це, і він ламається через деякі дивацтва в роботі FormSets внутрішньо. Є спосіб подолати проблему, але вона починає втрачати первісну елегантність ...
Карл Мейєр

5
Якщо нитка коментарів тут не має сенсу, це тому, що я щойно відредагував відповідь на використання Python functools.partialзамість Django django.utils.functional.curry. Вони роблять те ж саме, за винятком того, що functools.partialповертає виразний тип дзвінка, а не звичайну функцію Python, і partialтип не пов'язується як метод екземпляра, що чітко вирішує проблему, яку цей потік коментарів значною мірою присвячував налагодженню.
Карл Мейєр

81

Офіційний шлях до документа

Джанго 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
це має бути правильним способом зробити це зараз. прийнята відповідь спрацьовує і приємно, але хак
Junchao Gu

безумовно, найкраща відповідь і правильний спосіб це зробити.
янів14

Також працює у Django 1,11 docs.djangoproject.com/en/1.11/topics/forms/formsets/…
ruohola

46

Я буду будувати клас форми динамічно у функції, щоб він мав доступ до філії через закриття:

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

Як бонус, вам не доведеться переписувати набір запитів у полі опцій. Мінус у тому, що підкласифікація є трохи фанк. (Будь-який підклас повинен бути зроблений аналогічно.)

редагувати:

У відповідь на коментар ви можете зателефонувати на цю функцію про будь-яке місце, де ви б використовували назву класу:

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

Спасибі - що спрацювало. Я тримаюсь на тому, щоб позначити це як прийняте, тому що я сподіваюся, що є більш чистий варіант, оскільки робити це таким чином, безумовно, відчуває себе фанком.
Паоло Бергантіно

Позначення як прийняте, оскільки, мабуть, це найкращий спосіб зробити це. Почувається дивно, але робить трюк. :) Дякую.
Паоло Бергантіно

Карл Меєр, я думаю, чистіший спосіб, який ви шукали.
Джаррет Харді

Я використовую цей метод разом із Django ModelForms.
chefsmart

Мені подобається це рішення, але я не впевнений, як його використовувати в такому вигляді, як формат. Чи є у вас хороші приклади того, як це використовувати для перегляду? Будь-які пропозиції вдячні.
Джо J

16

Це те, що працювало для мене, 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):

Сподіваюся, це комусь допоможе, мені знадобилося досить довго, щоб це зрозуміти;)


1
Чи можете ви пояснити мені, навіщо staticmethodце потрібно тут?
fpghost

9

Мені подобається рішення про закриття за те, що він "чистіший" і більше пітонічний (тому відповідь +1 до mmarshall відповідь), але форми Django також мають механізм зворотного виклику, який можна використовувати для фільтрації наборів запитів у наборах форм.

Це також не задокументоване, що, на мою думку, є показником, який, можливо, не подобається девам Джанго.

Таким чином, ви в основному створюєте набір форматів однаково, але додаєте зворотний виклик

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

Це створює екземпляр класу, який виглядає приблизно так:

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

Це має дати вам загальну думку. Це трохи складніше зробити зворотний виклик таким об'єктним методом, але дає вам трохи більше гнучкості на відміну від простого зворотного виклику функції.


1
Дякую за відповідь. Я зараз використовую рішення mmarshall, і оскільки ви згодні, це більше Pythonic (те, чого я б не знав, оскільки це мій перший проект Python), я думаю, я дотримуюся цього. Хоча, безумовно, добре знати про зворотний дзвінок. Знову дякую.
Паоло Бергантіно

1
Дякую. Цей спосіб чудово працює з modelformset_factory. Я не міг домогтися інших способів належної роботи з наборами моделей, але цей спосіб був дуже простим.
Спайк

Функціонал каррі по суті створює закриття, чи не так? Чому ви кажете, що рішення @ mmarshall є більш пітонічним? Btw, дякую за вашу відповідь. Мені подобається такий підхід.
Джош

9

Я хотів розмістити це як коментар до відповіді Карла Майєрса, але оскільки для цього потрібні бали, я просто розмістив його тут. На це знадобилося 2 години, щоб розібратися, тому я сподіваюся, що це комусь допоможе.

Примітка про використання inlineformset_factory.

Я використовував це рішення, і я працював ідеально, поки не спробував це з inlineformset_factory. Я запускав Django 1.0.2 і отримав якесь дивне виключення KeyError. Я перейшов до останнього багажника, і він працював прямо.

Зараз я можу використовувати його подібне до цього:

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

Те ж саме стосується modelformset_factory. Дякую за цю відповідь!
thnee

9

Станом на комісію e091c18f50266097f648efc7cac2503968e9d217 у вівторок, 14 серпня 23:44:46 2012 +0200, прийняте рішення більше не може працювати.

Поточна версія функції django.forms.models.modelform_factory () використовує "тип побудови типу", викликаючи функцію type () у переданій формі для отримання типу метакласу, а потім використовуючи результат для побудови класового об'єкта свого введіть на льоту:

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

Це означає, що навіть curryед або partialоб’єкт, переданий замість форми, "змушує качку кусати вас", так би мовити: він викликатиме функцію з параметрами побудови ModelFormClassоб'єкта, повертаючи повідомлення про помилку ::

function() argument 1 must be code, not str

Щоб вирішити це, я написав функцію генератора, яка використовує закриття, щоб повернути підклас будь-якого класу, вказаного як перший параметр, який потім викликає super.__init__після updateвикористання kwargs з тими, що надходять у виклик функції генератора ::

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

Тоді у своєму коді ви будете називати заводську форму як:

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

застереження:

  • це отримало дуже мало тестування, принаймні поки що
  • надані параметри можуть зіткнутися та замінити ті, які використовуються будь-яким кодом, буде використовувати об'єкт, повернутий конструктором

Дякую, схоже, це дуже добре працює в Django 1.10.1 на відміну від деяких інших рішень тут.
fpghost

1
@fpghost майте на увазі, що принаймні до 1,9 (я все ще не на 1,10 з ряду причин), якщо все, що вам потрібно зробити, це змінити QuerySet, на якому побудована форма, ви можете оновити її на повернув MyFormSet, змінивши його атрибут .queryset перед його використанням. Менш гнучка, ніж цей метод, але набагато простіша для читання / розуміння.
RobM

3

Рішення Карла Мейєра виглядає дуже елегантно. Я спробував реалізувати його для модельних наборів. У мене було враження, що я не можу викликати статичні методи в класі, але такі незрозумілі дії:

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

На мою думку, якщо я роблю щось подібне:

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

Потім ключове слово "запит" поширюється на всі форми учасників мого набору. Я задоволений, але поняття не маю, навіщо це працює - це здається неправильним. Будь-які пропозиції?


Хммм ... Тепер, якщо я спробую отримати доступ до атрибуту форми екземпляра MyFormSet, він (правильно) повертає <функцію _curried> замість <MyForm>. Будь-які пропозиції щодо доступу до фактичної форми? Я намагався MyFormSet.form.Meta.model.
trubliphone

Ну, я повинен викликати функцію curried, щоб отримати доступ до форми. MyFormSet.form().Meta.model. Очевидно насправді.
trubliphone

Я намагався застосувати ваше рішення до моєї проблеми, але, думаю, я не повністю розумію всю вашу відповідь. Будь-які ідеї, якщо ваш підхід можна застосувати до мого питання тут? stackoverflow.com/questions/14176265 / ...
finspin

1

Я витратив деякий час, намагаючись вирішити цю проблему, перш ніж побачив цю публікацію.

Рішення, яке я придумав, було рішення про закриття (і це рішення, яке я раніше використовував у модельних формах Джанго).

Я спробував метод curry (), як описано вище, але мені просто не вдалося змусити його працювати з Django 1.0, тому врешті-решт я повернувся до методу закриття.

Метод закриття дуже акуратний, і лише незначна дивна ситуація полягає в тому, що визначення класу вкладається всередині подання чи іншої функції. Я думаю, що це, як мені дивно, є зависання від мого попереднього досвіду програмування, і я думаю, що хтось із фоном на більш динамічних мовах не зможе повіяти!


1

Мені довелося зробити подібну справу. Це схоже на curryрішення:

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

на основі цієї відповіді я знайшов більш чітке рішення:

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

І запускати його на виду як

formset_factory(form=ServiceForm.make_service_form(affiliate))

6
Django 1.9 зробив все це непотрібним, замість цього використовуйте form_kwargs.
Паоло Бергантіно

У моїй теперішній роботі нам потрібно використовувати спадщину django 1.7 ((
alexey_efimov

0

Я тут новачок, тому не можу додавати коментарів. Я сподіваюся, що і цей код буде працювати:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

що стосується додавання додаткових параметрів до набору форм BaseFormSetзамість форми.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.