开发者

django - dynamic form fieldsets

A form will be spitting out an unknown number of questions to be answered. each question contains a prompt, a value field, and a开发者_StackOverflow中文版 unit field. The form is built at runtime in the formclass's init method.

edit: each questions receives a unique prompt to be used as a label, as well as a unique list of units for the select element.

this seems a case perfect for iterable form fieldsets, which could be easily styled. but since fieldsets - such as those in django-form-utils are defined as tuples, they are immutable... and I can't find a way to define them at runtime. is this possible, or perhaps another solution?

Edit:

formsets with initial_data is not the answer - initial_data merely enables the setting of default values for the form fields in a formset. a list of items can't be sent to the choicefield constructor by way of initial_data.

...unless I'm wrong.


Check out formsets. You should be able to pass in the data for each of the N questions as initial data. Something along these lines:

question_data = []
for question in your_question_list:
    question_data.append({'prompt': question.prompt, 
                          'value': question.value, 
                          'units': question.units})
QuestionFormSet = formset_factory(QuestionForm, extra=2)
formset = QuestionFormSet(initial=question_data)


Old question but I am running into a similar problem. The closest thing that I have found so far is this snippet based of a post that Malcom did a couple years ago now. http://djangosnippets.org/snippets/1955/

The original snippet did not address the template side and splitting them up into fieldsets, but adding each form to its own fieldset should accomplish that.

forms.py

    from django.forms.formsets import Form, BaseFormSet, formset_factory, \
            ValidationError


    class QuestionForm(Form):
        """Form for a single question on a quiz"""
        def __init__(self, *args, **kwargs):
            # CODE TRICK #1
            # pass in a question from the formset
            # use the question to build the form
            # pop removes from dict, so we don't pass to the parent
            self.question = kwargs.pop('question')
            super(QuestionForm, self).__init__(*args, **kwargs)

            # CODE TRICK #2
            # add a non-declared field to fields
            # use an order_by clause if you care about order
            self.answers = self.question.answer_set.all(
                    ).order_by('id')
            self.fields['answers'] = forms.ModelChoiceField(
                    queryset=self.answers())


    class BaseQuizFormSet(BaseFormSet):
        def __init__(self, *args, **kwargs):
            # CODE TRICK #3 - same as #1:
            # pass in a valid quiz object from the view
            # pop removes arg, so we don't pass to the parent
            self.quiz = kwargs.pop('quiz')

            # CODE TRICK #4
            # set length of extras based on query
            # each question will fill one 'extra' slot
            # use an order_by clause if you care about order
            self.questions = self.quiz.question_set.all().order_by('id')
            self.extra = len(self.questions)
            if not self.extra:
                raise Http404('Badly configured quiz has no questions.')

            # call the parent constructor to finish __init__            
            super(BaseQuizFormSet, self).__init__(*args, **kwargs)

        def _construct_form(self, index, **kwargs):
            # CODE TRICK #5
            # know that _construct_form is where forms get added
            # we can take advantage of this fact to add our forms
            # add custom kwargs, using the index to retrieve a question
            # kwargs will be passed to our form class
            kwargs['question'] = self.questions[index]
            return super(BaseQuizFormSet, self)._construct_form(index, **kwargs)


    QuizFormSet = formset_factory(
        QuestionForm, formset=BaseQuizDynamicFormSet)

views.py

from django.http import Http404


    def quiz_form(request, quiz_id):
        try:
            quiz = Quiz.objects.get(pk=quiz_id)
        except Quiz.DoesNotExist:
            return Http404('Invalid quiz id.')
        if request.method == 'POST':
            formset = QuizFormSet(quiz=quiz, data=request.POST)
            answers = []
            if formset.is_valid():
                for form in formset.forms:
                    answers.append(str(int(form.is_correct())))
                return HttpResponseRedirect('%s?a=%s'
                        % (reverse('result-display',args=[quiz_id]), ''.join(answers)))
        else:
            formset = QuizFormSet(quiz=quiz)

        return render_to_response('quiz.html', locals())

template

{% for form in formset.forms %}
<fieldset>{{ form }}</fieldset>
{% endfor %}


I used the trick below to create a dynamic formset. Call the create_dynamic_formset() function from your view.

def create_dynamic_formset(name_filter):

    """
    -Need to create the classess dynamically since there is no other way to filter
    """
    class FormWithFilteredField(forms.ModelForm):
        type = forms.ModelChoiceField(queryset=SomeType.objects.filter(name__icontains=name_filter))

        class Meta:
            model=SomeModelClass

    return modelformset_factory(SomeModelClass, form=FormWithFilteredField)


Here is what I used for a similar case (a variable set of fieldsets, each one containing a variable set of fields).

I used the type() function to build my Form Class, and BetterBaseForm class from django-form-utils.

def makeFurnitureForm():
    """makeFurnitureForm() function will generate a form with
    QuantityFurnitureFields."""

    furnitures = Furniture.objects.all()
    fieldsets = {}
    fields = {}

    for obj in furnitures:
        # I used a custom Form Field, but you can use whatever you want.
        field = QuantityFurnitureField(name = obj.name)

        fields[obj.name] = field
        if not obj.room in fieldsets.keys():
            fieldsets[obj.room] = [field,]
        else:
            fieldsets[obj.room].append(field)

    # Here I use a double list comprehension to define my fieldsets
    # and the fields within.
    # First item of each tuple is the fieldset name.
    # Second item of each tuple is a dictionnary containing :
    #  -The names of the fields. (I used a list comprehension for this)
    #  -The legend of the fieldset.
    # You also can add other meta attributes, like "description" or "classes",
    # see the documentation for further informations.
    # I added an example of output to show what the dic variable
    # I create may look like.
    dic = [(name, {"fields": [field.name for field in fieldsets[name]], "legend" : name})
           for name in fieldsets.keys()]
    print(dic)
    # Here I return a class object that is my form class.
    # It inherits from both forms.BaseForm and forms_utils.forms.BetterBaseForm.
    return (type("FurnitureForm",
                 (forms.BaseForm, form_utils.forms.BetterBaseForm,),
                 {"_fieldsets" : dic, "base_fields" : fields,
                  "_fieldset_collection" : None, '_row_attrs' : {}}))

Here is an example of how dic may look like :

[('fieldset name 1',
    {'legend': 'fieldset legend 2',
     'fields' ['field name 1-1']}),
('fieldset name 2',
    {'legend': 'fieldset legend 2',
     'fields' : ['field 1-1', 'field 1-2']})]

I used BetterBaseForm rather than BetterForm for the same reason this article suggests to use BaseForm rather than Form.

This article is interesting even if it's old, and explains how to do dynamic forms (with variable set of fields). It also gives other ways to achieve dynamic forms.

It doesn't explain how to do it with fieldsets though, but it inspired me to find how to do it, and the principle remains the same.

Using it in a view is pretty simple :

return (render(request,'main/form-template.html', {"form" : (makeFurnitureForm())()}))

and in a template :

    <form method="POST" name="myform" action=".">
      {% csrf_token %}
      <div>
        {% for fieldset in form.fieldsets %}
        <fieldset>
          <legend>{{ fieldset.legend }}</legend>
          {% for field in fieldset %}
          <div>
            {% include "main/furniturefieldtemplate.html" with field=field %}
          </div>
          {% endfor %}
        </fieldset>
        {% endfor %}
      </div>
      <input type="submit" value="Submit"/>
    </form>
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜