Django Передача параметров пользовательской формы в форму

Это было исправлено в 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 отдельным формам таким образом?

Ответ 1

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 5

Мне нравится решение закрытия для того, чтобы быть "более чистым" и более 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

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

Ответ 6

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

Ответ 7

С момента совершения 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 ed или 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

Затем в вашем коде вы будете называть форму factory as::

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

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

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

Ответ 8

Решение Carl Meyer выглядит очень элегантно. Я попытался реализовать его для 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)

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

Ответ 9

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

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

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

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

Ответ 10

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

Ответ 11

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

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

как для добавления дополнительных параметров в formet BaseFormSet вместо формы.

Ответ 12

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

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