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

122

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

Теги:
forms
django-forms

12 ответов

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

  • 0
    Это не работает для меня. Я получаю сообщение об ошибке: AttributeError: у объекта '_curriedFormSet' нет атрибута 'get'
  • 0
    Я не могу продублировать эту ошибку. Это также странно, потому что у набора форм обычно нет атрибута 'get', поэтому кажется, что вы можете делать что-то странное в своем коде. (Также я обновил ответ, чтобы избавиться от таких странностей, как _curriedFormSet).
Показать ещё 17 комментариев
41

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

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)
    ...
  • 0
    Спасибо - это сработало. Я задерживаюсь на маркировке этого как принятого, потому что я надеюсь надеяться, что есть более чистый вариант, поскольку делать это таким образом определенно кажется странным.
  • 0
    Маркировка принята, так как, по-видимому, это лучший способ сделать это. Чувствует себя странно, но делает свое дело. :) Спасибо.
Показать ещё 4 комментария
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):

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

  • 0
    нвм ... у меня была опечатка (формы вместо формы ....)
  • 1
    Не могли бы вы объяснить мне, почему здесь нужен staticmethod ?
15

Django 1.9:

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

https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

  • 4
    это должен быть правильный способ сделать это сейчас. принятый ответ работает и хорошо, но это взломать
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 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))

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

  • это получило очень мало тестирования, по крайней мере на данный момент
  • предоставленные параметры могут конфликтовать и перезаписывать те, которые используются каким-либо кодом, будет использовать объект, возвращенный конструктором
  • 0
    Спасибо, похоже, очень хорошо работает в Django 1.10.1, в отличие от некоторых других решений здесь.
  • 0
    @fpghost имейте в виду, что, по крайней мере, до 1,9 (я все еще не на 1,10 по ряду причин), если все, что вам нужно сделать, это изменить QuerySet, на котором построена форма, вы можете обновить его на вернул MyFormSet, изменив его атрибут .queryset перед его использованием. Менее гибкий, чем этот метод, но гораздо проще для чтения / понимания.
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))
  • 0
    То же самое относится и к modelformset_factory . Спасибо за этот ответ!
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. Я не мог получить другие способы правильно работать с наборами моделей, но этот путь был очень простым.
Показать ещё 1 комментарий
3

Решение 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)

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

  • 0
    Хммм ... Теперь, если я пытаюсь получить доступ к атрибуту формы экземпляра MyFormSet, он (правильно) возвращает <function _curried> вместо <MyForm>. Любые предложения о том, как получить доступ к реальной форме, хотя? Я пробовал MyFormSet.form.Meta.model .
  • 0
    К сожалению ... Я должен вызвать функцию карри для доступа к форме. MyFormSet.form().Meta.model . На самом деле очевидно.
Показать ещё 1 комментарий
1

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

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

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

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

0

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

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))
  • 5
    Django 1.9 сделал все это ненужным, вместо этого используйте form_kwargs.
  • 0
    В моей текущей работе нам нужно использовать legacy django 1.7 ((
0

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

ServiceFormSet = formset_factory(ServiceForm, extra=3)

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню