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' ไม่มีแอตทริบิวต์ 'รับ'
Paolo Bergantino

ฉันไม่สามารถทำซ้ำข้อผิดพลาดนี้ได้ นอกจากนี้ยังเป็นเรื่องแปลกเพราะชุดรูปแบบมักจะไม่มีแอตทริบิวต์ 'รับ' ดังนั้นดูเหมือนว่าคุณอาจทำสิ่งที่แปลกในรหัสของคุณ (นอกจากนี้ฉันได้อัปเดตคำตอบพร้อมวิธีการกำจัดสิ่งแปลกประหลาดเช่น '_curriedFormSet')
คาร์ลเมเยอร์

ฉันกำลังทบทวนสิ่งนี้เพราะฉันต้องการให้โซลูชันของคุณใช้งานได้ ฉันสามารถประกาศ formset ที่ดี แต่ถ้าฉันพยายามที่จะพิมพ์มันทำ {{formset}} คือเมื่อฉันได้รับข้อผิดพลาด "ไม่มีแอตทริบิวต์ 'รับ'" มันเกิดขึ้นกับโซลูชันที่คุณให้ไว้ ถ้าฉันวนรอบ formset และพิมพ์แบบฟอร์มเป็น {{form}} ฉันได้รับข้อผิดพลาดอีกครั้ง ถ้าฉันวนซ้ำและพิมพ์เป็น {{form.as_table}} ตัวอย่างฉันได้รับตารางแบบฟอร์มที่ว่างเปล่าเช่น ไม่มีการพิมพ์ฟิลด์ ความคิดใด ๆ
Paolo Bergantino

คุณพูดถูกฉันขอโทษ การทดสอบก่อนหน้าของฉันยังไม่เพียงพอ ฉันติดตามสิ่งนี้ลงและมันเกิดขึ้นเนื่องจากความผิดปกติบางอย่างในวิธีที่ FormSets ทำงานภายใน ไม่มีทางที่จะหลีกเลี่ยงปัญหามี แต่มันเริ่มที่จะสูญเสียความสง่างามเดิม ...
คาร์ลเมเยอร์

5
หากกระทู้แสดงความคิดเห็นที่นี่ไม่ได้ทำให้รู้สึกว่ามันเป็นเพราะผมเพิ่งแก้ไขคำตอบที่จะใช้งูใหญ่functools.partialแทนของ django.utils.functional.curryDjango พวกเขาทำสิ่งเดียวกันยกเว้นว่าfunctools.partialจะส่งกลับชนิด callable ที่แตกต่างกันแทนฟังก์ชั่น Python ปกติและpartialประเภทไม่ได้ผูกเป็นวิธีการอินสแตนซ์ซึ่งแก้ปัญหาอย่างเรียบร้อยกระทู้ความคิดเห็นนี้ส่วนใหญ่จะทุ่มเทเพื่อการดีบัก
Carl Meyer

81

ทางเอกสารอย่างเป็นทางการ

Django 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


8
นี่ควรเป็นวิธีที่ถูกต้องในการทำตอนนี้ คำตอบที่ได้รับการยอมรับใช้ได้ดี แต่แฮ็ค
Junchao Gu

คำตอบที่ดีที่สุดและวิธีการแก้ไขที่ถูกต้องแน่นอน
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

ในฐานะโบนัสคุณไม่ต้องเขียนชุดคิวรีอีกครั้งในฟิลด์ตัวเลือก ข้อเสียคือ subclassing ค่อนข้างขี้ขลาดเล็กน้อย (คลาสย่อยใด ๆ จะต้องทำในลักษณะที่คล้ายกัน)

แก้ไข:

ในการตอบกลับความคิดเห็นคุณสามารถเรียกใช้ฟังก์ชันนี้เกี่ยวกับสถานที่ใดก็ได้ที่คุณจะใช้ชื่อคลาส:

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

ขอบคุณ - ที่ใช้งานได้ ฉันกำลังทำเครื่องหมายนี้ไว้เป็นที่ยอมรับเพราะฉันหวังว่าจะมีตัวเลือกที่สะอาดกว่าเดิม
เปาโล Bergantino

การทำเครื่องหมายเป็นที่ยอมรับเนื่องจากเห็นได้ชัดว่านี่เป็นวิธีที่ดีที่สุดในการทำ รู้สึกแปลก ๆ แต่ก็ทำอุบาย :) ขอบคุณ.
เปาโล Bergantino

ฉันคิดว่าคาร์ลเมเยอร์เป็นวิธีที่สะอาดกว่าที่คุณมองหา
Jarret Hardie

ฉันใช้วิธีนี้กับ Django ModelForms
chefsmart

ฉันชอบโซลูชันนี้ แต่ฉันไม่แน่ใจว่าจะใช้งานอย่างไรในมุมมองเหมือนชุดรูปแบบ คุณมีตัวอย่างที่ดีเกี่ยวกับวิธีใช้สิ่งนี้ในมุมมองหรือไม่? ข้อเสนอแนะใด ๆ ที่ชื่นชม
Joe J

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

หวังว่าจะช่วยใครซักคนเอาฉันนานพอที่จะคิดออก;)


1
คุณช่วยอธิบายให้ฉันฟังได้ไหมว่าทำไมถึงstaticmethodจำเป็นที่นี่?
fpghost

9

ฉันชอบวิธีการปิดการเป็น "สะอาด" และ Pythonic มากกว่า (ดังนั้น +1 ถึง mmarshall คำตอบ) แต่รูปแบบ Django ยังมีกลไกการโทรกลับที่คุณสามารถใช้สำหรับการกรองชุดแบบสอบถามในรูปแบบ

มันยังไม่ได้บันทึกซึ่งฉันคิดว่าเป็นตัวบ่งชี้ที่ devango Django อาจไม่ชอบมันมาก

โดยทั่วไปแล้วคุณสร้าง formset เหมือนกัน แต่เพิ่ม callback:

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 แรกของฉัน) ฉันเดาว่าฉันจะทำเช่นนั้น เป็นเรื่องดีที่ได้ทราบเกี่ยวกับการติดต่อกลับ ขอบคุณอีกครั้ง.
เปาโล Bergantino

1
ขอบคุณ. วิธีนี้ใช้งานได้ดีกับ modelformset_factory ฉันไม่สามารถทำงานอื่น ๆ ด้วย modelformsets ได้อย่างถูกต้อง แต่วิธีนี้ตรงไปตรงมามาก
ขัดขวาง

ฟังก์ชั่นของแกงเป็นหลักสร้างการปิดใช่มั้ย ทำไมคุณถึงบอกว่าวิธีการแก้ปัญหาของ @ mmarshall เป็น Pythonic มากกว่า ขอบคุณมากสำหรับคำตอบของคุณ ฉันชอบวิธีนี้
Josh

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 ส.ค. 14 23:44:46 2012 +0200 โซลูชั่นที่ยอมรับไม่สามารถทำงานได้อีกต่อไป

รุ่นปัจจุบันของ django.forms.models.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__หลังจากนั้น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

จากนั้นในรหัสของคุณคุณจะเรียกโรงงานแบบฟอร์มเป็น ::

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

คำเตือน:

  • สิ่งนี้ได้รับการทดสอบน้อยมากอย่างน้อยก็ในตอนนี้
  • พารามิเตอร์ที่ให้มาสามารถปะทะกันและเขียนทับสิ่งที่ใช้โดยรหัสใด ๆ ที่จะใช้วัตถุที่ส่งกลับโดยนวกรรมิก

ขอบคุณดูเหมือนว่าจะทำงานได้ดีมากใน Django 1.10.1 ซึ่งแตกต่างจากโซลูชันอื่น ๆ ที่นี่
fpghost

1
@fpghost โปรดจำไว้ว่าอย่างน้อยถึง 1.9 (ฉันยังไม่ได้อยู่ที่ 1.10 ด้วยเหตุผลหลายประการ) หากคุณต้องทำทั้งหมดคือเปลี่ยน QuerySet ตามแบบฟอร์มที่สร้างขึ้นคุณสามารถอัปเดตบน ส่งคืน MyFormSet โดยเปลี่ยนแอตทริบิวต์. queryset ก่อนใช้งาน มีความยืดหยุ่นน้อยกว่าวิธีนี้ แต่อ่าน / เข้าใจได้ง่ายกว่ามาก
RobM

3

วิธีการแก้ปัญหาของคาร์ลเมเยอร์ดูสง่างามมาก ฉันพยายามใช้มันเพื่อ 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)

จากนั้นคำหลัก "คำขอ" จะแพร่กระจายไปยังแบบฟอร์มสมาชิกทั้งหมดของชุดฟอร์มของฉัน ฉันพอใจ แต่ฉันไม่รู้ว่าทำไมสิ่งนี้ถึงได้ผล - ดูเหมือนผิด ข้อเสนอแนะใด ๆ


อืม ... ตอนนี้ถ้าฉันพยายามเข้าถึงคุณสมบัติของอินสแตนซ์ของ MyFormSet มัน (ถูกต้อง) จะส่งกลับ <function _curried> แทนที่จะเป็น <MyForm> ข้อเสนอแนะใด ๆ เกี่ยวกับวิธีการเข้าถึงแบบฟอร์มจริง แต่? ฉันพยายามMyFormSet.form.Meta.modelแล้ว
trubliphone

อ๊ะ ... ฉันต้องเรียกใช้ฟังก์ชั่น curried เพื่อที่จะเข้าถึงแบบฟอร์ม MyFormSet.form().Meta.model. ชัดเจนจริงๆ
trubliphone

ฉันพยายามใช้วิธีแก้ไขปัญหาของคุณแล้ว แต่ฉันคิดว่าฉันไม่เข้าใจคำตอบทั้งหมดของคุณ มีแนวคิดใดบ้างถ้าแนวทางของคุณสามารถนำไปใช้กับปัญหาของฉันได้ที่นี่ stackoverflow.com/questions/14176265/…
finspin

1

ฉันใช้เวลาพยายามหาปัญหานี้ก่อนที่จะเห็นการโพสต์นี้

วิธีแก้ปัญหาที่ฉันพบคือโซลูชันปิด (และเป็นวิธีแก้ปัญหาที่ฉันเคยใช้กับรูปแบบโมเดล Django)

ฉันลองใช้วิธีการแกง () ตามที่อธิบายไว้ข้างต้น แต่ฉันไม่สามารถใช้กับ 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))

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

สำหรับการเพิ่มพารามิเตอร์เพิ่มเติมให้กับ formset BaseFormSetแทนที่จะเป็นแบบฟอร์ม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.