I would like to know how to customize the Validation message when using UniqueConstraint to create unique constraints on multiple columns (including foreign keys) of Model in Django.
In Django, I am creating a system for parents to apply for school lunches for their children. An account exists for each parent, and one or more children are associated with one parent. When parents log in to the school lunch application screen, select their child and date, and press the school lunch application button to complete the application. The validation of this application form did not go well.
We use UniqueConstraints for multiple columns (child, date) in the Lunch model to avoid duplicate lunch requests for the same child on the same date. By default, the error message is confusing, so I tried to change the validation message, but it didn't work. Specifically, I created check_duplicate as a class method in the Lunch model and called it from the clean method of the form (ModelForm). At this time, both the validation message that I created and the default message are displayed on the actual screen.
・Message I created (child has already applied for yyyy/mm/dd school lunch.)
・Default message (Lunch with this child and date already exists.)
I want the validation message displayed on the screen below to be only the message I created.
source code
# models.py
from django.db import models
from django.utils.timezone import now
from django.contrib.auth import get_user_model
User = get_user_model()
class Child(models.Model):
name = models.CharField(max_length=128)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Lunch(models.Model):
child = models.ForeignKey(Child, on_delete=models.CASCADE)
date = models.DateField(default=now)
class Meta:
constraints = [
models.UniqueConstraint(fields=['child', 'date'],
name='unique_lunch_application',
)
]
@classmethod
def check_duplicate(cls, child, date):
return cls.objects.filter(child=child, date=date).exists()
# forms.py
from django import forms
from .models import Child, Lunch
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.contrib.auth import get_user_model
User = get_user_model()
class LunchForm(forms.ModelForm):
class Meta:
model = Lunch
fields = ('child', 'date',)
def __init__(self, user, *args, **kwargs):
super(LunchForm, self).__init__(*args, **kwargs)
self.fields['child'].queryset = Child.objects.filter(user=user)
def clean(self):
cleaned_data = super().clean()
child = cleaned_data.get('child')
date = cleaned_data.get('date')
if Lunch.check_duplicate(child=child, date=date):
_validation_messsage = f'{child} has already applied for {date} school lunch.'
raise ValidationError(
_(_validation_messsage), code="duplicated_lunch_application"
)
return cleaned_data
# views.py
from django.shortcuts import render, redirect
from .models import Child, Lunch
from .forms import LunchForm
from django.contrib.auth import get_user_model
User = get_user_model()
def lunch(request):
if request.method == 'POST':
form = LunchForm(request.user, request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('another view name')
else:
form = LunchForm(user=request.user)
return render(request, 'members/lunch.html', {'form': form}) # Urls are defined in urls.py.
<!-- HTML(DjangoTemplates) -->
<form action="" method="POST">
{{ form.non_field_errors }}
{% for field in form %}
<div class="field">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<span class="helptext">{{ field.help_text }}</span>
{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<div class="field">
<button type="submit">apply lunch</button>
</div>
{% csrf_token %}
</form>
tried
I found that the following implementation using Form instead of ModelForm works fine with other parts as is. (default validation message is not displayed) However, this time, the child column has to work as a foreign key, so it has not been resolved. It seems necessary to solve it in another way while using ModelForm, or to incorporate foreign keys in Form.
# forms.py
from django import forms
from .models import Child, Lunch
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from django.contrib.auth import get_user_model
User = get_user_model()
class LunchForm(forms.Form):
child = forms.CharField(max_length=16) # ←Actually, this part is a foreign key, so I can't write it like this.
date = forms.DateField()
def __init__(self, user, *args, **kwargs):
super(LunchForm, self).__init__(*args, **kwargs)
self.fields['child'].queryset = Child.objects.filter(user=user)
def clean(self):
cleaned_data = super().clean()
child = cleaned_data.get('child')
date = cleaned_data.get('date')
if Lunch.check_duplicate(child=child, date=date):
_validation_messsage = f'{child} has already applied for {date} school lunch.'
raise ValidationError(
_(_validation_messsage), code="duplicated_lunch_application"
)
return cleaned_data
version information
python 3.9.13 Django 4.1