可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a Django model that looks like this.
class Solution(models.Model):
'''
Represents a solution to a specific problem.
'''
name = models.CharField(max_length=50)
problem = models.ForeignKey(Problem)
description = models.TextField(blank=True)
date = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("name", "problem")
I use a form for adding models that looks like this:
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
My problem is that the SolutionForm
does not validate Solution
's unique_together
constraint and thus, it returns an IntegrityError
when trying to save the form. I know that I could use validate_unique
to manually check for this but I was wondering if there's any way to catch this in the form validation and return a form error automatically.
Thanks.
回答1:
I solved this same problem by overriding the validate_unique()
method of the ModelForm:
def validate_unique(self):
exclude = self._get_validation_exclusions()
exclude.remove('problem') # allow checking against the missing attribute
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError, e:
self._update_errors(e.message_dict)
Now I just always make sure that the attribute not provided on the form is still available, e.g. instance=Solution(problem=some_problem)
on the initializer.
回答2:
As Felix says, ModelForms are supposed to check the unique_together
constraint in their validation.
However, in your case you are actually excluding one element of that constraint from your form. I imagine this is your problem - how is the form going to check the constraint, if half of it is not even on the form?
回答3:
I managed to fix this without modifying the view by adding a clean method to my form:
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def clean(self):
cleaned_data = self.cleaned_data
try:
Solution.objects.get(name=cleaned_data['name'], problem=self.problem)
except Solution.DoesNotExist:
pass
else:
raise ValidationError('Solution with this Name already exists for this problem')
# Always return cleaned_data
return cleaned_data
The only thing I need to do now in the view is to add a problem property to the form before executing is_valid
.
回答4:
the solution from @sttwister is right but can be simplified.
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def clean(self):
cleaned_data = self.cleaned_data
if Solution.objects.filter(name=cleaned_data['name'],
problem=self.problem).exists():
raise ValidationError(
'Solution with this Name already exists for this problem')
# Always return cleaned_data
return cleaned_data
As a bonus you do not retreive the object in case of duplicate but only check if it exists in the database saving a little bit of performances.
回答5:
With the help of Jarmo's answer, the following seems to work nicely for me (in Django 1.3), but it's possible I've broken some corner case (there are a lot of tickets surrounding _get_validation_exclusions
):
class SolutionForm(forms.ModelForm):
class Meta:
model = Solution
exclude = ['problem']
def _get_validation_exclusions(self):
exclude = super(SolutionForm, self)._get_validation_exclusions()
exclude.remove('problem')
return exclude
I'm not sure, but this seems like a Django bug to me... but I'd have to look around the previously-reported issues.
Edit: I spoke too soon. Maybe what I wrote above will work in some situations, but not in mine; I ended up using Jarmo's answer directly.
回答6:
You will need to do something like this:
def your_view(request):
if request.method == 'GET':
form = SolutionForm()
elif request.method == 'POST':
problem = ... # logic to find the problem instance
solution = Solution(problem=problem) # or solution.problem = problem
form = SolutionForm(request.POST, instance=solution)
# the form will validate because the problem has been provided on solution instance
if form.is_valid():
solution = form.save()
# redirect or return other response
# show the form
回答7:
If you want the error message to be a associated with the name
field (and appear next to it):
def clean(self):
cleaned_data = super().clean()
name_field = 'name'
name = cleaned_data.get(name_field)
if name:
if Solution.objects.filter(name=name, problem=self.problem).exists():
cleaned_data.pop(name_field) # is also done by add_error
self.add_error(name_field, _('There is already a solution with this name.'))
return cleaned_data