Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 263
Cody Django Avatar asked Apr 12 '10 21:04

Cody Django


People also ask

What are Fieldsets in Django?

fieldsets is a list of two-tuples, in which each two-tuple represents a <fieldset> on the admin form page. (A <fieldset> is a “section” of the form.)

How do you make a field not editable in Django?

disabled. The disabled boolean argument, when set to True , disables a form field using the disabled HTML attribute so that it won't be editable by users. Even if a user tampers with the field's value submitted to the server, it will be ignored in favor of the value from the form's initial data.

How do you make a field optional in Django?

If you want to allow blank values in a date field (e.g., DateField , TimeField , DateTimeField ) or numeric field (e.g., IntegerField , DecimalField , FloatField ), you'll need to use both null=True and blank=True . Save this answer. Show activity on this post. Use null=True and blank=True in your model.


2 Answers

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)
like image 156
istruble Avatar answered Oct 15 '22 03:10

istruble


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 %}
like image 40
Justin Hamade Avatar answered Oct 15 '22 04:10

Justin Hamade