In Django admin interface, when submitting a form which has an optional integer field, I want to show a validation error message when user enters an incorrect type (string). However when user enters a string into this field and submits the form, Django recognizes it as an incorrect type and changes the value to None and successfully validates the form. Therefore there is no way to find out if the field was left blank or if the user entered incorrect type. Even when I try to get clean data from custom form (clean_<field_name>) it always returns None.
Is there a way to show a validation error for an optional field? Or can I somehow get the value that was actually entered into the field?
Thank you for your help.
I tried to overwrite default behaviour with custom form and tried to get clean data from the field but nothing works, it always returns None.
Here is the model:
class Condition(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
code = models.CharField()
common_name_en = models.CharField()
ordering = models.IntegerField(blank=True, null=True)
common_name_cs = models.CharField()
eval_time_frame = models.ForeignKey('ConditionEvalTime', models.DO_NOTHING, db_column='eval_time_frame')
unit = models.CharField(blank=True, null=True)
metric_extreme_low = models.FloatField(blank=True, null=True)
metric_extreme_high = models.FloatField(blank=True, null=True)
metric_scale = models.FloatField(blank=True, null=True)
class Meta:
managed = False
db_table = 'planner"."condition'
def __str__(self) -> str:
return self.common_name_en
And here is my admin file:
def validate_integer(value):
print("before check " + value)
if not isinstance(value, int):
print("after check " + value)
raise forms.ValidationError("This field must be an integer.")
class ConditionForm(forms.ModelForm):
class Meta:
model = Condition
exclude = []
ordering = forms.IntegerField(required=False, validators=[validate_integer])
@admin.register(Condition)
class ConditionAdmin(admin.ModelAdmin):
def get_related_models(self, obj):
"""
Return all models that have 'one-to-many' relation to Condition model
"""
related_models = []
related_fields = obj._meta.get_fields()
for field in related_fields:
if field.is_relation and field.one_to_many:
related_model = field.related_model
related_models.append(related_model._meta.object_name.lower())
return related_models
def has_delete_permission(self, request, obj=None):
"""
Overwrite delete permission if object has relations
"""
if obj:
for related_model in self.get_related_models(obj):
if getattr(obj, f"{related_model}_set").exists():
messages.warning(request, f"Can't delete: {obj.common_name_en} - {obj.id}")
return False
return super().has_delete_permission(request, obj)
form = ConditionForm
fields = [field.name for field in Condition._meta.fields]
readonly_fields = ["id"]
I tried to use custom validators but it doesn't work either, the validator just doesn't get run.
When the ModelForm
renders the model's IntegerField
, it creates an <input type="number">
HTML tag. Depending on how you look at it, this input type has a problematic behaviour, particularly in Firefox. While Chrome will block any character that's not a valid floating-point number, Firefox does not. Firefox will let you type into the field any character you want, which I suspect is the browser you're using. The value
of the field will be whatever you typed into it as long as it's a valid input. However, the moment an invalid character is entered, the value
will be set to an empty string
, which is what Firefox does and the the HTML spec states:
The value sanitization algorithm is as follows: If the value of the element is not a valid floating-point number, then set it to the empty string instead.
I tried logging the input's value on keyup
event, and that's indeed the behaviour Firefox implements.
Because this "emptying of the value" happens on an input event, it will be empty on form submission (if you don't set the novalidate
attribute, Firefox will not let you submit the form if the field has an invalid input. This is why you can't access the invalid entry of the field in your backend (assuming you disabled browser validation), just as you can't access it with JavaScript via the value
attribute: it will always be empty.
In Django admin
, the change_form
that is used to render the add/change
forms has a novalidate
attribute set, so the browser's form validation is disabled. That's why when you save your Condition
object with an invalid ordering
input, the ordering
value will be an empty string, and cast as None by Django.
There are developers who have voiced out their distaste for < input type="number">
tag. See this SO blog post for example. And the UK government does not use it for similar reasons.
As the UK government suggests, one possible workaround would be to use <input type="text" inputmode="numeric" pattern="[0-9]*"
. Which means the ordering
will have to be a Charfield and handle interger validation yourself (in case browser validation is bypassed or disabled), which can be easily done by overriding the model's clean
method. But with your current setup, it is impossible to achieve the functionality you want.
I hope that helps.