Stefan Weiss
Stefan Weiss

Reputation: 461

Dynamic Multiwidget/MultivalueField from Model

The beginning is simple:

class Question(models.Model):
    question_string = models.CharField(max_length=255)
    answers = models.CharField(max_length=255)

answers are json of list of strings e.g ['Yes', 'No']. Number of answers is dynamic. The challenge for me now is to write a form for this model.

Current state is:

class NewQuestionForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(NewQuestionForm, self).__init__(*args, **kwargs)

        if self.instance:
            self.fields['answers'] = AnswerField(num_widgets=len(json.loads(self.instance.answers)))

    class Meta:
        model = Question
        fields = ['question']
        widgets = {
            'question': forms.TextInput(attrs={'class': "form-control"})
        }


class AnswerField(forms.MultiValueField):
    def __init__(self, num_widgets, *args, **kwargs):
        list_fields = []
        list_widgets = []
        for garb in range(0, num_widgets):
            field = forms.CharField()
            list_fields.append(field)
            list_widgets.append(field.widget)

        self.widget = AnswerWidget(widgets=list_widgets)
        super(AnswerField, self).__init__(fields=list_fields, *args, **kwargs)

    def compress(self, data_list):
        return json.dumps(data_list)

class AnswerWidget(forms.MultiWidget):

    def decompress(self, value):
        return json.loads(value)

The problem is: i get 'the JSON object must be str, not 'NoneType'' in template with '{{ field }}'

What is wrong?

Upvotes: 1

Views: 1364

Answers (1)

Stefan Weiss
Stefan Weiss

Reputation: 461

I found the problem. I forgot to add 'answers' to class Meta 'fields'.

So my example of dynamic Multiwidget created from Model is:

class NewQuestionForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        # need this to create right number of fields from POST
        edit_mode = False
        if len(args) > 0:
            edit_mode = True
            answer_fields = 0
            for counter in range(0, 20):
                answer_key = "answers_" + str(counter)
                if args[0].get(answer_key, None) is not None:
                    answer_fields = counter + 1
                else:
                    break
        super(NewQuestionForm, self).__init__(*args, **kwargs)
        if edit_mode:
            self.fields['answers'] = AnswerField(num_widgets=answer_fields, required=False)
        # get number of fields from DB
        elif 'instance' in kwargs:
            self.fields['answers'] = AnswerField(num_widgets=len(json.loads(self.instance.answers)), required=False)
        else:
            self.fields['answers'] = AnswerField(num_widgets=1, required=False)

    class Meta:
        model = Question
        fields = ['question', 'answers']
        widgets = {
            'question': forms.TextInput(attrs={'class': "form-control"})
        }

    def clean_answers(self):
        temp_data = []
        for tdata in json.loads(self.cleaned_data['answers']):
            if tdata != '':
                temp_data.append(tdata)
        if not temp_data:
            raise forms.ValidationError('Please provide at least 1 answer.')
        return json.dumps(temp_data)

'clean_answers' has 2 porposes: 1. Remove empty answers. 2. I failed to set required attribute on first widget. So i check here at least 1 answer exists

class AnswerWidget(forms.MultiWidget):

    def decompress(self, value):
        if value:
            return json.loads(value)
        else:
            return ['']


class AnswerField(forms.MultiValueField):
    def __init__(self, num_widgets, *args, **kwargs):
        list_fields = []
        list_widgets = []
        for loop_counter in range(0, num_widgets):
            list_fields.append(forms.CharField())
            list_widgets.append(forms.TextInput(attrs={'class': "form-control"}))
        self.widget = AnswerWidget(widgets=list_widgets)
        super(AnswerField, self).__init__(fields=list_fields, *args, **kwargs)

    def compress(self, data_list):
        return json.dumps(data_list)

Upvotes: 1

Related Questions