Django Передача пользовательских параметров формы в Formset

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'
Паоло Бергантино,
Я не могу продублировать эту ошибку. Это также странно, потому что у formset обычно нет атрибута get, поэтому кажется, что вы делаете что-то странное в своем коде. (Также я обновил ответ, чтобы избавиться от таких странностей, как _curriedFormSet).
Карл Мейер
Я возвращаюсь к этому, потому что хотел бы, чтобы ваше решение работало. Я могу объявить набор форм в порядке, но если я пытаюсь напечатать его, выполняя {{formset}}, я получаю сообщение об ошибке "has not attribute 'get'". Это происходит с любым решением, которое вы предоставили. Если я перебираю набор форм и печатаю формы как {{form}}, я снова получаю сообщение об ошибке. Если я, например, зациклюсь и напечатаю как {{form.as_table}}, я получу пустые таблицы форм, т.е. поля не печатаются. Любые идеи?
Паоло Бергантино
Ты прав, прости; мои предыдущие испытания не зашли достаточно далеко. Я отследил это, и оно сломалось из-за некоторых странностей в том, как FormSets работают внутри. Есть способ обойти эту проблему, но она начинает терять первоначальную элегантность ...
Карл Мейер,
5
Если ветка комментариев здесь не имеет смысла, это потому, что я только что отредактировал ответ, чтобы использовать Python functools.partial вместо Django django.utils.functional.curry. Они делают то же самое, за исключением того, что functools.partialвозвращает обычный вызываемый тип вместо обычной функции Python, и этот partialтип не привязывается как метод экземпляра, который аккуратно решает проблему, которую этот поток комментариев в основном посвятил отладке.
Карл Мейер
81

Официальный документ Way

Джанго 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
источник
8
это должен быть правильный способ сделать это сейчас. принятый ответ работает и это хорошо, но это взломать
Junchao Гу
безусловно, лучший ответ и правильный способ сделать это.
yaniv14
Также работает в 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
Мне нравится это решение, но я не уверен, как использовать его в виде набора форм. У вас есть хорошие примеры того, как использовать это в представлении? Любые предложения приветствуются.
Джо Дж.
16

Вот что сработало для меня, Джанго 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):

Надеюсь, это кому-нибудь поможет, мне понадобилось много времени, чтобы понять это;)

RIX
источник
1
Не могли бы вы объяснить мне, почему staticmethodздесь нужно?
fpghost
9

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

Это также не задокументировано, что, по-моему, является показателем, который может не понравиться разработчикам 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 более Pythonic? Кстати, спасибо за ваш ответ. Мне нравится этот подход.
Джош
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)

Это означает, что даже curryed или partialобъект, переданный вместо формы, «заставляет утку вас укусить», так сказать: он вызовет функцию с параметрами конструкции ModelFormClassобъекта, возвращая сообщение об ошибке:

function() argument 1 must be code, not str

Чтобы обойти это, я написал функцию генератора, которая использует замыкание для возврата подкласса любого класса, указанного в качестве первого параметра, который затем вызывает super.__init__после вызова updatekwargs с теми, которые указаны в вызове функции генератора:

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

предостережения:

  • это получило очень мало испытаний, по крайней мере сейчас
  • предоставленные параметры могут конфликтовать и перезаписывать те, которые используются любым кодом, который будет использовать объект, возвращаемый конструктором
RobM
источник
Спасибо, похоже, очень хорошо работает в 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)

Затем ключевое слово «request» распространяется на все формы-члены моего набора форм. Я доволен, но я понятия не имею, почему это работает - кажется, неправильно. Какие-либо предложения?

trubliphone
источник
Хммм ... Теперь, если я пытаюсь получить доступ к атрибуту формы экземпляра MyFormSet, он (правильно) возвращает <function _curried> вместо <MyForm>. Любые предложения о том, как получить доступ к реальной форме, хотя? Я пытался MyFormSet.form.Meta.model.
Трублифон
К сожалению ... Я должен вызвать функцию карри для доступа к форме. MyFormSet.form().Meta.model, На самом деле очевидно.
Трублифон
Я пытался применить ваше решение к моей проблеме, но я думаю, что я не до конца понимаю весь ваш ответ. Любые идеи, если ваш подход может быть применен к моей проблеме здесь? stackoverflow.com/questions/14176265/…
finspin
1

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

Решением, которое я придумал, было решение замыкания (и это решение, которое я использовал раньше с модельными формами Django).

Я попробовал метод 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))
alexey_efimov
источник
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вместо формы.

Филамер сьюн
источник