I have written a function that accept as input a string and do some validation tasks on it and also changes the value.
def validate(str):
# do validation. If any error, raise Validation error
# modify value of str
return str
I want to use this function as a validator for some django model field. I know how to do it. My problem is that in addition to validation I want the modified value, i.e. return value of function, to be saved in field.
The models.py module is not right place to do this as input validation is usually done in forms. But still you can do it in Model.save() method:
# models.py
def validate(str):
# do validation. If any error, raise Validation error
# modify value of str
return str
class YourModel(models.Model):
...
field_to_validate = models.CharField(max_length=100)
...
def save(self, **kwargs):
try:
self.field_to_validate = validate(self.field_to_validate)
except YourValidationError:
self.field_to_validate = ''
super(YourModel, self).save(**kwargs)
Related
Im trying to create a custom validator that must be able to validate different conditions given a type
def validator(value, type, param):
match type:
case 'regex_validator':
if not re.search(param, value):
raise ValidationError()
case 'max_length':
if value > param:
raise ValidationError()
and my question is: How can I pass the function the form value? The examples I have seen don't implicitly pass the value to the validator.
I want to do declare the the form fiel like this:
forms.CharField(validators=[validator(value, x['type'], x['param'] for x in field_validators])
Django does not allow to make a multi fields validation with field validators. You have to do your validation in the clean_FIELDNAME function form.
class FormExample():
field = forms.CharField()
def clean_field(self, data):
if XXX:
validator = RegexValidator()
elif YYYY:
validator = MaxLengthValidator()
validator(data)
Or you can create a custom field with overrided validate function for making your custom choice of validator (https://docs.djangoproject.com/en/4.1/ref/forms/validation/#form-field-default-cleaning)
My goal is to perform some additional action when a user changes a value of a existing record.
I found on_model_change() in the docs and wrote the following code:
def on_model_change(self, form, model, is_created):
# get old and new value
old_name = model.name
new_name = form.name
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
My expectation was that the model parameter would represent the record before the new values from the form was applied. It did not. Instead i found that model always had the same values as form, meaning that the if statement never was fulfilled.
Later i tried this:
class MyView(ModelView):
# excluding the name field from the form
form_excluded_columns = ('name')
form_extra_fields = {
# and adding a set_name field instead
'set_name':StringField('Name')
}
...
def on_model_change(self, form, model, is_created):
# getting the new value from set_name instead of name
new_name = form.set_name
...
Although this solved my goal, it also caused a problem:
The set_name field would not be prefilled with the existing name, forcing the user to type the name even when not intending to change it
I also tried doing db.rollback() at the start of on_model_change() which would undo all changes done by flask-admin, and make model represent the old data. This was rather hacky and lead my to reimplement alot of flask admin code myself, which got messy.
What is the best way to solve this problem?
HOW I SOLVED IT
I used on_form_prefill to prefill the new name field instead of #pjcunningham 's answer.
# fill values from model
def on_form_prefill(self, form, id):
# get track model
track = Tracks.query.filter_by(id=id).first()
# fill new values
form.set_name.data = track.name
Override method update_model in your view. Here is the default behaviour if you are using SqlAlchemy views, I have added some notes to explain the model's state.
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
# at this point model variable has the unmodified values
form.populate_obj(model)
# at this point model variable has the form values
# your on_model_change is called
self._on_model_change(form, model, False)
# model is now being committed
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# model is now committed to the database
self.after_model_change(form, model, False)
return True
You'll want something like the following, it's up to you where place the check, I've put it after the model has been committed:
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
old_name = model.name
new_name = form.name.data
# continue processing the form
form.populate_obj(model)
self._on_model_change(form, model, False)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# the model got committed now run our check:
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
self.after_model_change(form, model, False)
return True
There are similar methods you can override for create_model and delete_model.
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = Child
fields = '__all__'
class ParentSerializer(serializers.ModelSerializer):
"""
Serializer for task
"""
def validate_title(self, data):
if not data.get('title'):
raise serializers.ValidationError('Please set title')
return data
Validate Function is not called when Post ,Also how can i give custom errors to ChildSerializer ,
I ran into a similar problem where my custom validation field was not being called. I was writing it to bypass an incorrect DRF validation (more details shown below, but not necessary for the answer).
Looking into the DRF source code I found my problem: DRF always validates your field using its code before validating with your custom code.
''' rest-framework/serializers.py '''
for field in fields:
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
# DRF validation always runs first!
# If DRF validator throws, then custom validation is not called
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
# this is your custom validation
validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = get_error_detail(exc)
Answer: Custom validators cannot be used to bypass DRF's validators, as they will always run first and will raise an exception before you can say it is valid.
(for those interested, the validation error I hit was like this: ModelSerializer used for ModelA, which has a OneToOne relation to ModelB. ModelB has a UUID for its pk. DRF throws the error '53abb068-0286-411e-8729-0174635c5d81' is not a valid UUID. when validating, which is incorrect, and really infuriating.)
Your ParentSerializer validation method has some issues. Assumes that there is a title field in your ParentSerializer model. For field level validation, you will get the field instead of whole data. That is validate_title function should have title(title field of the data) as parameter not data. So you dont have to check data.get('title') for the existance of title. Reference
class ParentSerializer(serializers.ModelSerializer):
"""
Serializer for task
"""
def validate_title(self, title):
if not title:
raise serializers.ValidationError('Please set title')
return title
In addition to #sean.hudson's answer I was trying to figure out how to override the child serializer validation.
It might be possible to "skip" or more accurately ignore children serializer validation errors, by overriding to_internal_value of the ParentSerialzer:
class ParentSerializer(serializers.ModelSerializer):
children = ChildSerializer(many=True)
def to_internal_value(self, *args, **kwargs):
try:
# runs the child serializers
return super().to_internal_value(*args, **kwargs)
except ValidationError as e:
# fails, and then overrides the child errors with the parent error
return self.validate(self.initial_data)
def validate(self, attrs):
errors = {}
errors['custom_override_error'] = 'this ignores and overrides the children serializer errors'
if len(errors):
raise ValidationError(errors)
return attrs
class Meta:
model = Parent
My problem was that I had my own custom to_internal_value method. Removing it fixed the issue.
class EventSerializer(serializers.Serializer):
end_date = serializers.DateTimeField(format=DATE_FORMAT, required=True)
start_date = serializers.DateTimeField(format=DATE_FORMAT, required=True)
description = serializers.CharField(required=True)
def validate_start_date(self, start_date):
return start_date
def validate_end_date(self, end_date):
return end_date
# def to_internal_value(self, data):
# if data.get('start_date', False):
# data['start_date'] = datetime.strptime(data['start_date'], DATE_FORMAT)
# if data.get('end_date', False):
# data['end_date'] = datetime.strptime(data['end_date'], DATE_FORMAT)
# return data
I would like to add what the official documentation says, I hope it can be of help.
Field-level validation
You can specify custom field-level validation by adding .validate_<field_name> methods to your Serializer subclass. These are similar to the .clean_<field_name> methods on Django forms.
These methods take a single argument, which is the field value that requires validation.
Your validate_<field_name> methods should return the validated value or raise a serializers.ValidationError. For example:
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
content = serializers.CharField()
def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value`
In my django app, this is my validator.py
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
def validate_url(value):
url_validator = URLValidator()
url_invalid = False
try:
url_validator(value)
except:
url_invalid = True
try:
value = "http://"+value
url_validator(value)
url_invalid = False
except:
url_invalid = True
if url_invalid:
raise ValidationError("Invalid Data for this field")
return value
which is used to validate this :
from django import forms
from .validators import validate_url
class SubmitUrlForm(forms.Form):
url = forms.CharField(label="Submit URL",validators=[validate_url])
When I enter URL like google.co.in, and print the value right before returning it from validate_url, it prints http://google.co.in but when I try to get the cleaned_data['url'] in my views, it still shows google.co.in. So where does the value returned by my validator go and do I need to explicitly edit the clean() functions to change the url field value??
The doc says the following:
The clean() method on a Field subclass is responsible for running to_python(), validate(), and run_validators() in the correct order and propagating their errors. If, at any time, any of the methods raise ValidationError, the validation stops and that error is raised. This method returns the clean data, which is then inserted into the cleaned_data dictionary of the form.
I am still not sure where the validator return value goes and if it is possible to change cleaned_data dict using the validator.
From the docs:
A validator is merely a callable object or function that takes a value
and simply returns nothing if the value is valid or raises a
ValidationError if not.
The return value is simply ignored.
If you want to be able to modify the value you may use clean_field on the forms as described here:
class SubmitUrlForm(forms.Form):
url = ...
def clean_url(self):
value = self.cleaned_data['url']
...
return updated_value
Validators are only about validating the data, hence that is why the return value of the validator gets ignored.
You are looking for data "cleaning" (transforming it in a common form). In Django, Forms are responsible for data cleaning.
Use URLField. It validates value and prepends http if neccessary.
I have a Model which has a restricts the number of decimal places:
lat = models.DecimalField(max_digits=8, decimal_places=5, null=True, blank=True)
From it I create a ModalForm however I would like to cap the number of decimal places if they submitted something with more than 5 places. So I do a custom clean method:
def clean_lat(self):
lat = self.cleaned_data['lat']
return round(lat, 4)
But it still raises a ValidationError that I have more decimal places then allowed. What am I doing wrong?
There are two option to solve this.
First one is to override your modelform init method like this
def __init__(self, *args, **kwargs):
super(TestForm, self).__init__(*args, **kwargs)
self.fields['lat'].decimal_places = None
self.fields['lat'].max_digits = None
This will disable the decimal places and max digit valididation by modelform. Then your clean_lat method should ensure ensure form data validation. Model will still truncate/ roundoff/ validate decimal value.
Second option is that remove max_digits=8, decimal_places=5 from model and ensure validation in your forms clean_lat method. This can create problem if object is saved without using the ModelForm.
the form.is_valid method do a very simple thing:
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
self._post_clean()
I suggest you get into the source code(django/forms/forms.py), and the reason will be located quickly.