Skip to content

Commit

Permalink
Available direct access to Model.full_clean() api, rename parameter i…
Browse files Browse the repository at this point in the history
…nclude=None to extra_include=None
  • Loading branch information
GiuseppeNovielli committed Aug 23, 2024
1 parent 867793c commit 2302e3c
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 32 deletions.
2 changes: 1 addition & 1 deletion drf_fullclean/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = 'DRF full clean'
__version__ = '0.0.1'
__version__ = '0.0.2'
__author__ = 'Giuseppe Novielli'
__license__ = 'MIT'
__copyright__ = 'Copyright 2024 Giuseppe Novielli'
Expand Down
125 changes: 95 additions & 30 deletions drf_fullclean/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@

from django.core.exceptions import ValidationError as DjangoValidationError, FieldDoesNotExist

from django.forms.models import model_to_dict

# DEBUG
def print_debug(message):
drf_full_clean = getattr(settings, 'DRF_FULL_CLEAN', {})
if not drf_full_clean.get('DEBUG', False):
return
print(message)

class FullCleanModelSerializer(serializers.ModelSerializer):
"""
Giuseppe Novielli 2024 (Copyright)
Expand All @@ -13,26 +22,28 @@ class FullCleanModelSerializer(serializers.ModelSerializer):
https://www.django-rest-framework.org/community/3.0-announcement/#differences-between-modelserializer-validation-and-modelform
https://github.com/encode/django-rest-framework/discussions/7850#discussioncomment-8380135
"""
def is_valid(self, raise_exception=False, include=None, validate_unique=True, *args, **kwargs):
drf_full_clean_debug = getattr(settings, 'DRF_FULL_CLEAN_DEBUG', False)

if drf_full_clean_debug:
print('FullCleanModelSerializer is_valid -> {}'.format(self.Meta.model))
def is_valid(self, raise_exception=False, exclude=None, validate_unique=True, extra_include=None, *args, **kwargs):
print_debug('\nFullCleanModelSerializer is_valid -> {}'.format(self.Meta.model))

is_valid = super().is_valid(raise_exception=raise_exception)
if not is_valid:
return is_valid

return self.is_valid_model(raise_exception=raise_exception, include=include, validate_unique=validate_unique, *args, **kwargs)
return self.is_valid_model(raise_exception=raise_exception,
exclude=exclude, validate_unique=validate_unique,
extra_include=extra_include,
*args, **kwargs)


def is_valid_model(self, raise_exception=False, include=None, validate_unique=True, *args, **kwargs):
if include and not isinstance(include, dict):
raise TypeError('Expected dict for argument "include", but got: %r' % include)
def is_valid_model(self, raise_exception=False, exclude=None, validate_unique=True, extra_include=None, *args, **kwargs):
if extra_include and not isinstance(extra_include, dict):
raise TypeError('Expected dict for argument "extra_include", but got: %r' % extra_include)

obj = self.model_instance(self.Meta.model, self.validated_data, self.instance, self.partial, extra_include, **kwargs)

obj = self.model_instance(self.Meta.model, self.validated_data, self.instance, self.partial, include, **kwargs)
print('Instance to FullClean -> {}'.format(model_to_dict(obj)))
if obj:
errors = self.model_validation(obj, include, validate_unique, **kwargs)
errors = self.model_validation(obj, exclude, validate_unique, extra_include, **kwargs)
else:
raise Exception('Nested serializers are not supported.')

Expand All @@ -44,28 +55,29 @@ def is_valid_model(self, raise_exception=False, include=None, validate_unique=Tr
#

#INSTANCE
def model_instance(self, model_class, validated_data, instance=None, partial=False, include=None, **kwargs):
def model_instance(self, model_class, validated_data, instance=None, partial=False, extra_include=None, **kwargs):
if not instance:
return self.model_instance_create(model_class, validated_data, include, **kwargs)
return self.model_instance_update(model_class, validated_data, instance, partial, include, **kwargs)
return self.model_instance_create(model_class, validated_data, extra_include, **kwargs)
return self.model_instance_update(model_class, validated_data, instance, partial, extra_include, **kwargs)


def model_instance_create(self, model_class, validated_data, include=None, **kwargs):
def model_instance_create(self, model_class, validated_data, extra_include=None, **kwargs):

try:
i = model_class(**validated_data)
except:
return

#Add fields value that are not in validated_data
if include:
for key,value in include.items():
if extra_include:
for key,value in extra_include.items():
setattr(i, key, value)

return i



def model_instance_update(self, model_class, validated_data, instance, partial=False, include=None, **kwargs):
def model_instance_update(self, model_class, validated_data, instance, partial=False, extra_include=None, **kwargs):
try:
for field in instance._meta.fields:
if field.name not in validated_data:
Expand All @@ -75,33 +87,86 @@ def model_instance_update(self, model_class, validated_data, instance, partial=F
return

#Add fields value that are not in validated_data
if include:
for key,value in include.items():
if extra_include:
for key,value in extra_include.items():
setattr(instance, key, value)
return instance


#

#VALIDATION
def model_validation_method(self, object, exclude=None, validate_unique=True, **kwargs):
exclude_key = []
def _get_validation_exclusions(self, instance=None):
"""
Return a list of field names to exclude from model validation.
https://github.com/encode/django-rest-framework/blob/2.4.8/rest_framework/serializers.py#L939C5-L956C26
"""
# cls = self.opts.model
cls = self.Meta.model
opts = cls._meta.concrete_model._meta
exclusions = [field.name for field in opts.fields + opts.many_to_many]

for field_name, field in self.fields.items():
field_name = field.source or field_name
if (
field_name in exclusions
and not field.read_only
and (field.required or hasattr(instance, field_name))
and not isinstance(field, serializers.Serializer)
):
exclusions.remove(field_name)
return exclusions


def model_validation_method(self, object, exclude=None, validate_unique=True, extra_include=None, **kwargs):
fields_to_exclude = self._get_validation_exclusions(self.instance)
print_debug('serializer fields excluded -> {}'.format(fields_to_exclude))

if exclude:
for key, value in exclude.items():
fields_to_exclude.extend(exclude)

print_debug('model exclude -> {}'.format(exclude))

if extra_include:
for key, value in extra_include.items():
try:
field = object._meta.get_field(key)
exclude_key.append(key)
if field.is_relation and bool(field.validators):
raise Exception('Unsupported validation! Field {} is a relation that contains validators that needs the database id. Try to move validations\'s logic into clean() method, but analize object\'s fields instead make a query to the database, that validators need'.format(key))
fields_to_exclude.append(key)

if (
field.is_relation
and
bool(field.validators)
and
(
value.pk is None
or
value.id is None
)
):
raise Exception('Unsupported validation! Field {} is a relation that contains validators that needs the database id. \
Try to move validations\'s logic into clean() method, but analize object\'s fields instead make a query to the database, \
that validators need'.format(key))

except FieldDoesNotExist:
pass

object.full_clean(exclude=exclude_key, validate_unique=validate_unique)

exclude = list(set(fields_to_exclude))
print_debug('model full_clean exclude final -> {}'.format(exclude))

l = []
if extra_include:
for key, value in extra_include.items():
l.append({key: model_to_dict(value)})
print_debug('extra include -> {}'.format(l))

object.full_clean(exclude=exclude, validate_unique=validate_unique)




def model_validation(self, object, exclude=None, validate_unique=True,**kwargs):
def model_validation(self, object, exclude=None, validate_unique=True, extra_include=None, **kwargs):
try:
self.model_validation_method(object, exclude, validate_unique, **kwargs)
self.model_validation_method(object, exclude, validate_unique, extra_include, **kwargs)
except DjangoValidationError as exc:
return serializers.as_serializer_error(exc)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_version(package):
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()

with open('LICENSE.md', "r", encoding="utf-8") as f:
with open('LICENSE', "r", encoding="utf-8") as f:
license = f.read()


Expand Down

0 comments on commit 2302e3c

Please sign in to comment.