Django custom validator with multiple inputs - python

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)

Related

How to receive get parameter in viewset in django restframe work?

For the part of developing an API using restframe work and DJango, I need to receive few parameters through get method in 'list' function of my Viewset. In client side they are sending data as query prams, I somehow managed to get the data using 'request.query_params.get()' but received data is text format instead of bool, I had to convert this to boolean . see the code how I did this
if 'status' in (request.query_params):
"""receiving input argument is string need to convert that to boolean for further processing"""
input_status=(request.query_params.get('status', None)=='true')
is there any better way to getting data without losing datatype. is it possible to receive 'bool' or 'int' values directly in get parameter?
My model and view set classes are given below
class ModuleType(models.Model):
"""
This module type class used to represent each module of the application. It is inherited from
django content type which holds the complete model informations
"""
#this active field is true this module will be active on our user controller
content_type = models.OneToOneField(
ContentType,
on_delete=models.CASCADE,
related_name='status',
null=True
)
active=models.BooleanField(default=False)
class Meta:
db_table = 'module_types'
My Viewset
class ContentTypeViewSet(viewsets.ModelViewSet):
"""
This api deals all operations related with module management
You will have `list`, `create`, `retrieve`,
update` and `destroy` actions.
Additionally we also provide an action to update status.
"""
queryset = ContentType.objects.all()
serializer_class = ContentTypeSerializer
permission_classes = [permissions.AllowAny]
"""
Over ride list method to list values
"""
def list(self, request):
status='Sucess'
message='details of all modules included'
print(request)
try:
if 'status' in (request.query_params):
#input argument is striing need to convert that to boolean
input_status=(request.query_params.get('status', None)=='true')
validated_value= self.validate_input_params({'status':input_status})
if (validated_value==1):
all_modules = ContentType.objects.filter(status__active=input_status)
message='All value set status as '+str(input_status)
else:
message='Failed to validate all input values'
status='fail'
return Response({"status":status,"data":[],"message":message})
else:
all_modules=ContentType.objects.all()
serializer = self.get_serializer(
all_modules,
many=True
)
return Response({"status":status,"data":serializer.data,'message':message})
except:
message='unknown exception'
status='Fail'
return Response({"status":status,"data":[],"message":message})
Your view will read query params as string.
I suggest you use query_params = dict(request.GET.items())
That way you can parse parameters easier by using dict.

Serializer validate function is not called DRF

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`

validate and save in django field

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)

Where is a django validator function's return value stored?

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.

Suppress "field should be unique" error in Django REST framework

I have a model like
class MyModel(models.Model):
uuid = models.CharField(max_length=40, unique=True)
and a serializer
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ('uuid')
And I want to receive JSON with MyModel object but it can be existing objects. So, when I use serializer.is_valid() with data about existing object it gives me an error:
for record in request['records']:
# request - body of JSON request,
# 'records' - array of records I want to add or update
serializer = MyModelSerializer(data=record)
if serializer.is_valid():
# Do stuff
serializer.save()
Error:
{"uuid":["This field must be unique."]}
Is there a way to separate behavior for new and existing objects? Particularly, I want to create new MyModel object if it's not it database yet and update existing MyModel object if it's present.
You are basically overloading a single entry point of your REST API by trying to both create new instances and update existing instances using a POST request. In addition, it seems you are trying to create and update multiple instances simultaneously within a single POST request.
Django REST Framework (DRF) expects a POST request to only create new instances. Therefore, sending an existing instance record triggers a unique constraint violation for the uuid field since DRF tries to create that record as a new instance, as the existing instance already has that uuid value.
A solution to make your REST API more "RESTful" would be to separate the creation and updating of records into POST and PUT requests respectively. It is unclear if you are using the generic API views provided by DRF, but you can use the CreateAPIView to POST new instances, then create a separate UpdateAPIView to PUT and/or PATCH existing instances. Even better you could allow retrieval via GET for both of these endpoints using the generic views ListCreateAPIView and RetrieveUpdateAPIView.
Finally, for handling bulk requests (i.e. multi-instances in a single request) you can either override the built-in view methods or use a 3rd-party package such as django-rest-framework-bulk.
I had a situation where I had a deep create method, with 2 levels of hierarchy above the end point, that it was important that all models were idempotent.
I override the validation in the serializer, and created it by hand.
It is important that you add the field to the class at the top (otherwise the validator won't be run)
class ParticipantSerializer(serializers.HyperlinkedModelSerializer):
device = DeviceSerializer(required=False)
uuid = serializers.CharField()
def validate_uuid(self, value):
if value is not None and isinstance(value, basestring) and len(value) < 256:
return value
else:
if value is not None:
raise serializers.ValidationError("UUID can't be none")
elif isinstance(value, basestring):
raise serializers.ValidationError("UUID must be a string")
elif len(value) < 256:
raise serializers.ValidationError("UUID must be below 256 characters")
else:
raise serializers.ValidationError("UUID has failed validation")
class Meta:
model = Participant
fields = ("uuid", "platform", "device")

Categories

Resources