0

I have a model with a foreign key and a unique constraint as follows:

class Menu(models.Model):
    tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    date_menu = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['tournament', 'name', 'date_menu'], name="unique_name_menu")
        ]

I would like to create a form to add instance of Menu. However the value of tournament is set by the URL of the page. I do not want the user to be able to set it.

For this I use a modelForm, excluding the tournament field :

class MenuForm(forms.ModelForm):

    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        exclude = ['tournament']

Here is my view :

def add_menu(request, tournament_slug):
    tournament = get_object_or_404(Tournament, slug=tournament_slug)
    form = MenuForm(request.POST or None)
    
    if form.is_valid():
        menu_id = form.save(commit=False)
        menu_id.tournament = Tournament.objects.get(pk=1)
        menu_id.save()  # I get the integrity error only here

        return HttpResponseRedirect(reverse('admin'))

    return render(request, "view.html", {'form': form, 'formset': formset, "tournament": tournament})

My problem is that when I call the .is_valid() function on this form the uniqueness condition cannot be checked as the tournament field is not set. As a result I get an integrity error when calling the save function in the view.

The question is : how can link the Menu instance created by the form to add the tournament field before checking if it's valid? If it's not the right way of doing it, how can I check the uniqueness of the model instance and return the corresponding errors to the template when needed?

I tried including the tournament field as hidden field in the view, it works but I don't know if that's the best way of doing it...

1 Answer 1

1

You should simply instantiate the form with an unsaved instance of Menu so your view should be like:

def add_menu(request, tournament_slug):
    tournament = get_object_or_404(Tournament, slug=tournament_slug)
    if request.method == 'POST':
        form = MenuForm(request.POST, instance=Menu(tournament=tournament))
        if form.is_valid():
            menu_id = form.save()
            return HttpResponseRedirect(reverse('admin'))
    else:
        form = MenuForm(instance=Menu(tournament=tournament))
    return render(request, "view.html", {'form': form, "tournament": tournament})

Also the form calls _get_validation_exclusions() and excludes fields not present in the form from validation. You can try to override validate_unique to overcome this:

class MenuForm(forms.ModelForm):

    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        exclude = ['tournament']
    
    def validate_unique(self):
        exclude = self._get_validation_exclusions()
        if 'tournament' in exclude:
            exclude.remove('tournament') # Make sure `tournament` gets validated
        try:
            self.instance.validate_unique(exclude=exclude)
        except ValidationError as e:
            self._update_errors(e)

Note: I changed your view structure to avoid using MenuForm(request.POST or None) which is an antipattern. (Forms can be valid even if nothing is sent in the POST data, with the way you write such forms would be considered invalid).

Edit: As discussed in the comments perhaps the option of a hidden and disabled field is much better than overriding the forms validate_unique method:

class MenuForm(forms.ModelForm):
    tournament = forms.ModelChoiceField(
        queryset=Tournament.objects.all(),
        widget=forms.HiddenInput(),
        disabled=True
    )
    date_menu = forms.DateField(initial=datetime.datetime.now())

    class Meta:
        model = Menu
        fields = ['tournament', 'name', 'date_menu']
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks! But it does not work (yet)... Calling form.is_valid() returns true, whereas calling form.instance.validate_unique() returns the uniqueness error...
@Thombou slight oversight, I have edited the answer for that. Although this simply replaces the validate_unique and adds some code to it. Perhaps adding something to the clean method of the form might be better.
Yes, thanks. It now works. Is that the best way of doing this operation and are hidden fields a bad practice in general in that case?
@Thombou hidden fields can be tampered with. Perhaps a field that is both hidden and disabled would be the best solution here. (Disabled fields are not posted with the form data and even if posted are ignored in favor of the initial data by the form).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.