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

это было исправлено в 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 является зарегистрированным пользователем. Это работает по назначению.

моя проблема в том, что теперь я хочу превратить эту единственную форму в набор форм. Чего я не могу понять, так это как я могу передать партнерскую информацию в отдельные формы при создании formset с. Согласно документам, чтобы сделать formset из этого, мне нужно сделать что-то вроде этого:

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

и тогда мне нужно создать его так:

formset = ServiceFormSet()

теперь, как я могу передать affiliate=запрос.присоединяться к индивидуальным формам таким образом?

12 ответов


Я хотел бы использовать functools.частичный и functools.обертывания:

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 (т. е. затрудняя подкласс).


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

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

в качестве бонуса вам не нужно переписывать queryset в поле option. Недостатком является то, что подклассы немного фанки. (Любой подкласс должен быть сделан аналогичным образом.)

edit:

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

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

Путь Официального Документа

Джанго 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


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

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


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

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

таким образом, вы в основном создаете свой formset то же самое, но добавляете обратный вызов:

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

Это должно дать вам общее представление. Это немного сложнее сделать обратный вызов объектным методом, как это, но дает вам немного больше гибкости, в отличие от выполнения простого обратного вызова функции.


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

по состоянию на фиксацию e091c18f50266097f648efc7cac2503968e9d217 на Вт Aug 14 23:44: 46 2012 +0200 принятое решение больше не может работать.

текущая версия django.формы.модели.функция modelform_factory() использует "метод построения типа", вызывая функцию type () в переданной форме для получения типа metaclass, а затем используя результат для построения класса-объекта его типа на лету::

# 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__ после updateing кварги с теми, которые поставляются на вызов функции генератора::

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

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

  • это получило очень небольшое тестирование, по крайней мере, на данный момент
  • предоставленные параметры могут конфликтовать и перезаписывать те, которые используются любым кодом, который будет использовать объект, возвращенный конструктором

решение Карла Мейера выглядит очень элегантно. Я попытался реализовать его для modelformsets. У меня создалось впечатление, что я не могу назвать staticmethods внутри класса, но необъяснимо работает следующее:

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" распространяется на все формы-члены моего набора форм. Мне приятно, но я понятия не имею, почему это работает - это кажется неправильным. Есть предложения?


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

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

Я попробовал метод curry (), как описано выше, но я просто не мог заставить его работать с Django 1.0, поэтому в конце концов я вернулся к методу закрытия.

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


мне пришлось сделать подобную вещь. Это похоже на 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), ... )

Я новичок здесь, поэтому я не могу добавить комментарий. Надеюсь, этот код тоже будет работать:

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

Как добавить дополнительные параметры в formset с это BaseFormSet вместо формы.


на основе ответ Я нашел более четкое решение:

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