andilabs
andilabs

Reputation: 23321

keeping django models clean method validating foreign key object and using ModelForm save

In my model's clean method I validate if given in foreign key exhibitor is_premium and also validate that he does not have more then MAX_DISCOUNTS_PER_EXHIBITOR active objects.

It works perfectly fine in django admin. Both when adding as well when editing.

I would like to get it working in my custom (non-django-admin) views. I am using ModelForms.

first approach

I assign exhibitor in view, to initially commit=False saved object.

And it crashes with DoesNotExist: Discount has no exhibitor, because clean is executed, but exhibitor is not yet assigned. What should be the proper way of implementing it?

models.py

class Discount(models.Model):
    exhibitor = models.ForeignKey(
        'core_backend.Exhibitor', related_name='discounts')
    is_active = models.BooleanField(default=False, verbose_name=u"Aktywny")
    title = models.TextField(verbose_name=u"Tytuł")

    def clean(self):

        if not self.exhibitor.is_premium:
            raise ValidationError(
                u'Discounts only for premium')

        count = Discount.objects.filter(
            is_active=True,
            exhibitor=self.exhibitor
        ).count()

        if not self.pk:
            # newly added
            count = count + (1 if self.is_active else 0)
        else:
            # edited
            discount = Discount.objects.get(pk=self.pk)
            if discount.is_active and not self.is_active:
                count = count - 1
            elif not discount.is_active and self.is_active:
                count = count + 1

        if count > settings.MAX_DISCOUNTS_PER_EXHIBITOR:
            raise ValidationError(
                u'Max %s active discounts' % (
                    settings.MAX_DISCOUNTS_PER_EXHIBITOR
                )
            )

forms.py

class DiscountForm(forms.ModelForm):

    class Meta:
        model = Discount
        fields = (
            'title',
            'description',
            'is_activa',
        )

views.py

def add_discount(request, fair_pk, exhibitor_pk):
    fair = get_object_or_404(Fair, pk=fair_pk)
    exhibitor = get_object_or_404(
        Exhibitor, fair=fair, pk=exhibitor_pk, user=request.user)

    if request.method == 'GET':

        form = DiscountForm()

        return render(request, 'new_panel/add_discount.html', {
            'exhibitor': exhibitor,
            'discount_form': form,
        })

    if request.method == 'POST':
        form = DiscountForm(data=request.POST)

        if form.is_valid():
            discount = form.save(commit=False)
            discount.exhibitor = exhibitor
            discount.save()
            return redirect(reverse(
                'discounts_list',
                kwargs={"fair_pk": fair.pk, 'exhibitor_pk': exhibitor.pk}
            ))
        return render(request, 'new_panel/add_discount.html', {
            'exhibitor': exhibitor,
            'discount_form': form,
        })

Second approach

I was trying also different approach with separate form used for POST, but it crashes with same error:

forms.py

class DiscountFormAdd(forms.ModelForm):

    class Meta:
        model = Discount
        fields = (
            'title',
            'exhibitor',
            'is_active',
        )

    def __init__(self, exhibitor, *args, **kwargs):
        self.exhibitor = exhibitor
        super(DiscountFormAdd, self).__init__(*args, **kwargs)

    def save(self, commit=False):
        discount = super(DiscountFormAdd, self).save(commit=False)
        discount.exhibitor = self.exhibitor

        if commit:
            discount.save()

        return discount

views.py

def add_discount(request, fair_pk, exhibitor_pk):
    fair = get_object_or_404(Fair, pk=fair_pk)
    exhibitor = get_object_or_404(
        Exhibitor, fair=fair, pk=exhibitor_pk, user=request.user)

    if request.method == 'GET':

        form = DiscountForm()

        return render(request, 'new_panel/add_discount.html', {
            'exhibitor': exhibitor,
            'discount_form': form,
        })

    if request.method == 'POST':
        form = DiscountFormAdd(data=request.POST, exhibitor=exhibitor)

        if form.is_valid():
            discount = form.save(commit=True)

            return redirect(reverse(
                'discounts_list',
                kwargs={"fair_pk": fair.pk, 'exhibitor_pk': exhibitor.pk}
            ))
        return render(request, 'new_panel/add_discount.html', {
            'exhibitor': exhibitor,
            'discount_form': form,
        })

I would like to stick with model clean validation, rather than jumping into forms clean. If django admin is able to do it, then it is surely possible to implemented in custom modelforms.

Upvotes: 3

Views: 6511

Answers (1)

Emma
Emma

Reputation: 1081

When assigning exhibitor using self.exhibitor = exhibitor, this sets a property on your form which has nothing to do with the content for the form.

To actually set the exhibitor, use the following code instead:

import copy
.
.
.
    if request.method == 'POST':
        # request.POST is an immutable QueryDict so it needs to be copied
        form_data = copy.copy(request.POST)
        form_data['exhibitor'] = exhibitor.id
        form = DiscountFormAdd(data=form_data)

and entirely drop setting the exhibitor inside your form's __init__

This way, your form's data (which is used to create your Discount) object will be correct and can be cleaned

[Edit] How to create a hidden input field in a ModelForm:

class DiscountFormAdd(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(DiscountFormAdd, self).__init__(*args, **kwargs)
        self.fields['exhibitor'].widget = forms.HiddenInput()

    ...

or you can also use the widgets attribute of the Meta class

class DiscountFormAdd(forms.ModelForm):

    class Meta:
        model = Discount
        widgets = {'exhibitor': forms.HiddenInput()}
        ...

Upvotes: 2

Related Questions