0

Imagine that we are building a polls website, like the one in the Django tutorial.

There are two types of models. A question, and a choice. Each choice has a foreign key to a question. We wish to ensure that every question has at least two choices, but no more than 5 choices.

When creating a Question you also create the choices at the same time. Thus both objects will be saved simultaneously. How can you validate that the Question object has 2-5 choices with a relation to it when it is saved?

Since you cannot set a relation on an unsaved object, I'm not sure how this should be implemented.
The first solution that came to my mind was setting up a pre_save hook on the question object, but as I just said the object hasn't been saved yet at that point, and thus the choice objects cannot have a relation to it.
The second solution I thought of is setting up a post_save hook, where the Question is saved, then the choices are saved, and then it is checked to see whether the Question has 2-5 choices. This solution seemed awfully hacky, and I'm sure there's a better way.

How can you get this kind of functionality?
If it makes a difference I'm on Django 1.8 with PostgreSQL 9.4.

dcgoss
  • 2,147
  • 3
  • 20
  • 31
  • Try using a formset for Options or handle Question and Options in a single view. – Lorenzo Peña Jul 02 '15 at 19:44
  • @LorenzoPeña but let's say you are saving two options simultaneously to a newly saved Question object. One of them will try to save, but then your validation will raise an error because there cannot be only one option saved. In order for your method to work both objects need to save at the exact same time, which is not going to happen. – dcgoss Jul 02 '15 at 19:48
  • @LorenzoPeña You could do the form sets, but what if you have a JSON API that doesn't necessarily have access to form validation? – dcgoss Jul 02 '15 at 19:50
  • If you are handling the create/update of all options for a given Question in a single view, you could definitely validate the number of options that are being stored, be it in a local database or a remote API. Moreover, if you handle a Question and its Options in a single view, you can even prevent the Question from being created, otherwise you'd have to accept empty Questions while you handle Options somewhere else. – Lorenzo Peña Jul 02 '15 at 19:53
  • If you control the API, make it so that it handles options in batches. So no endpoint to create a single option, but to update the list of options. If you do not control the API, do not make the API call unless you have the number of options that you require. – Lorenzo Peña Jul 02 '15 at 19:57

1 Answers1

1

You can do this type of validation by overriding clean on a formset that you can inherit from:

from django.forms.models import BaseInlineFormSet


class RequiredFormSet(BaseInlineFormSet):

    def clean(self):
        for error in self.errors:
            # If any errors exist, return
            if error:
                return

        completed_formsets = 0
        for cleaned_data in self.cleaned_data:
            # only count the form if it's not being deleted
            if cleaned_data and not cleaned_data.get('DELETE', False):
                completed_formsets += 1

        if completed_formsets < 2 or completed_formsets > 5:
            raise forms.ValidationError("You must enter between "
                "2 and 5 {}".format(self.model._meta.verbose_name))

If you need to call an API with your post data after that, you can override the save, or do it in your, whatever's best for your project.

The minimum / maximum number of forms could come from settings, or be passed in when the formset is instantiated instead of hard-coding them in this example.

Brandon Taylor
  • 33,823
  • 15
  • 104
  • 144
  • This looks good, but the issue is with the API. Any person with access to HTTP can use an API, and therefore bypass form validation. Even if you make a user authenticate themselves before allowing them to POST, they can still authenticate from any client with their own login credentials. Perhaps Django Rest Framework supports [receiving multiple objects in one request](http://stackoverflow.com/questions/14666199/how-do-i-create-multiple-model-instances-with-django-rest-framework)? That way you could check the number of choices before you save. Good idea with the choice number setting. – dcgoss Jul 03 '15 at 05:16
  • I have very little experience with Django Rest Framework, but I would think it would have a way to do bulk inserts, and since it's based on the same model/form structure, there may be a way to hook this type of code in. – Brandon Taylor Jul 03 '15 at 12:07