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

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

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

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

formset = ServiceFormSet()

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

12 ответов

Решение

Я бы использовал 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.0:

ArticleFormSet = formset_factory(MyArticleForm)
formset = ArticleFormSet(form_kwargs={'user': request.user})

https://docs.djangoproject.com/en/2.0/topics/forms/formsets/

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

что касается добавления дополнительных параметров в форму набора 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))
Другие вопросы по тегам