hobbes3
hobbes3

Reputation: 30218

How do I programmatically set fields in ModelForm in Django?

From this question I want to convert my form from regular Form to ModelForm so I can take advantage of instance parameter in ModelForm.

Here is my current form code:

class OrderDetailForm(forms.Form):
    def __init__(
        self,
        user,
        can_edit_work_type=None,
        can_edit_vendor=None,
        can_edit_note=None,
        *args,
        **kwargs
    ):
        super(OrderDetailForm, self).__init__(*args, **kwargs)

        if can_edit_work_type:
            self.fields['work_type'] = forms.ChoiceField(choices=Order.WORK_TYPE_CHOICES)
        if can_edit_vendor:
            self.fields['vendor'] = forms.ModelChoiceField(
                queryset=Vendor.objects.all(),
                empty_label="Choose a vendor",
            )
        if can_edit_note:
            self.fields['note'] = forms.CharField(widget=forms.Textarea)

    def clean(self):
        super(OrderDetailForm, self).clean()

        if 'note' in self.cleaned_data:
            if len(self.cleaned_data['note']) < 50:
                self._errors['note'] = self.error_class([u"Please enter a longer note."])

                del self.cleaned_data['note']

        return self.cleaned_data

As you can see, I have some if statements that determine whether the fields even show you in the forms (logically it means certain users can only edit certain parts of the fields).

How would I do that in ModelForm? I understand fields to be a tuple, so it can't be appended like I did in Form. So I want to do something like

class OrderDetailForm(forms.ModelForm):
    class Meta:
        model = Order
        # fields = ('work_type', 'vendor', 'note') I can't do that since I need to be able to control it. See below.

        # Can I control widgets even if that field doesn't exist?
        widgets = {
            'note': forms.Textarea(),
        }

    def __init__(
        self,
        user,
        can_edit_work_type=None,
        can_edit_vendor=None,
        can_edit_note=None,
        *args,
        **kwargs
    ):
        super(OrderDetailForm, self).__init__(*args, **kwargs)

        fields = []

        if can_edit_work_type:
            fields.append('work_type')
        if can_edit_vendor:
            fields.append('vendor')
        if can_edit_note:
            fields.append('note')

        self.Meta.fields = tuple(fields) # Does this work?

    def clean(self):
        super(OrderDetailForm, self).clean()

        if 'note' in self.cleaned_data:
            if len(self.cleaned_data['note']) < 50:
                self._errors['note'] = self.error_class([u"Please enter a longer note."])

                del self.cleaned_data['note']

        return self.cleaned_data

Is that possible? How do you control the fields in ModelForm?

Upvotes: 4

Views: 8368

Answers (2)

Skylar Saveland
Skylar Saveland

Reputation: 11454

The ModelForm api is very similar to that of the regular Form. The advantage is that you now get model validation in addition to conveniences like default widgets, the instance kwarg, and the save method.

fields attr is still dict-like. You can see fields getting built by the metaclass here. Then, going through the inheritance and calling super() in the BaseModelForm.__init__, we arrive at a deepcopy of the declared fields, originally a SortedDict. This is common to Form and ModelForm, both subclasses of BaseForm.

Put the fields in the exclude and add them the way you are doing in the original __init__.

Clean them the same way.

Then, you can override the save method: you can call super() to get the object back and deal with the data in cleaned_data however you want.

class OrderDetailForm(forms.ModelForm):
    # regular fields, not based on bools
    # ...

    class Meta:
        model = Order
        exclude = ('work_type', 'vendor', 'note')
        # or fields = (...other fields )


    def __init__(
        self,
        user,
        can_edit_work_type=None,
        can_edit_vendor=None,
        can_edit_note=None,
        *args,
        **kwargs,
    ):
        super(OrderDetailForm, self).__init__(*args, **kwargs)

        if can_edit_work_type:
            self.fields['work_type'] = forms.ChoiceField(
                                       choices=Order.WORK_TYPE_CHOICES)
        if can_edit_vendor:
            self.fields['vendor'] = forms.ModelChoiceField(
                queryset=Vendor.objects.all(),
                empty_label="Choose a vendor",
            )
        if can_edit_note:
            self.fields['note'] = forms.CharField(widget=forms.Textarea)

    def clean(self):
        # I never call super() in clean .. do I? .. hmmm
        # maybe I should or is sth magic going on?
        # alternately,
        # data = self.cleaned_data
        # let's call super though
        data = super(OrderDetailForm, self).clean()

        if 'note' in data:
            if len(data['note']) < 50:
                # I raise a validation error so .is_valid() comes back False
                # form.errors happens magically ...
                raise forms.ValidationError("Not long enough ...")

        return data

    def save(self, *args, **kwargs):
        data = self.cleaned_data
        # maybe do some stuff here
        # ...

        # commit=True or commit=False could be important
        order = super(OrderDetailForm, self).save(*args, **kwargs)

        if 'note' in data:
            order.note = data['note']

        # ... do other stuff

        # probably ...
        order.save()

        # respect how model forms work.
        return order

Upvotes: 2

Wang Bin
Wang Bin

Reputation: 606

Another possible way is generate a inline form class in the view to exclude fields based on the request, for example, define a normal model form for Order model, called OrderDetailForm:

class OrderDetailForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ('work_type', 'vendor', 'note') 
        widgets = {
            'note': forms.Textarea(),
        }

In the view, for example, edit order, create a customized form based on the OrderDetailForm:

def edit(request, order_id):
    order = Order.objects.get(pk=order_id)
    can_edit_work_type = bool(request.REQUEST.get('can_edit_work_type', False))
    can_edit_vender = bool(request.REQUEST.get('can_edit_vender', False))
    can_edit_note = bool(request.REQUEST.get('can_edit_note', False))

    exclude_fields = []

    if not can_edit_work_type:
        exclude_fields.append('work_type')

    if not can_edit_vender:
        exclude_fields.append('vender')

    if not can_edit_note:
        exclude_fields.append('note')

    class CustomizedOrderForm(OrderDetailForm):
        class Meta:
            model = Order
            exclude = tuple(exclude_fields)

    if request.method == 'POST':
        form = CustomizedOrderForm(instance=order, data=request.POST)
        if form.is_valid():
            form.save()
    else:
        form = CustomizedOrderForm(instance=order)
    return render(request, 'order_form.html', {'form': form})

Upvotes: 5

Related Questions