diff --git a/drf_fullclean/__init__.py b/drf_fullclean/__init__.py index b6f19b2..c41b4f0 100644 --- a/drf_fullclean/__init__.py +++ b/drf_fullclean/__init__.py @@ -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' diff --git a/drf_fullclean/serializers.py b/drf_fullclean/serializers.py index 12137ec..3f0d95b 100644 --- a/drf_fullclean/serializers.py +++ b/drf_fullclean/serializers.py @@ -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) @@ -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.') @@ -44,13 +55,13 @@ 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) @@ -58,14 +69,15 @@ def model_instance_create(self, model_class, validated_data, include=None, **kwa 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: @@ -75,8 +87,8 @@ 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 @@ -84,24 +96,77 @@ def model_instance_update(self, model_class, validated_data, instance, partial=F # #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) \ No newline at end of file diff --git a/setup.py b/setup.py index f01f0b1..8fdd46d 100644 --- a/setup.py +++ b/setup.py @@ -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()