Skip to content

Clean Validation Mixin

CleanValidationMixin

Enable model clean validation in serializer.

Source code in saritasa_drf_tools/serializers/mixins/clean_validation_mixin.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class CleanValidationMixin:
    """Enable model clean validation in serializer."""

    def get_instance(
        self,
        attrs: dict[str, typing.Any],
    ) -> typing.Any:
        """Get instance depending on request."""
        if self.instance is not None:  # type: ignore
            # if it's update request
            return copy.deepcopy(self.instance)  # type: ignore
        model = self.Meta.model  # type: ignore
        # If attrs have `id` data, get instance form db
        # if it is a create request, we return empty instance
        if instance_id := attrs.get("id"):
            return model.objects.filter(pk=instance_id).first() or model()
        return model()

    def prepare_instance(
        self,
        attrs: dict[str, typing.Any],
    ) -> typing.Any:
        """Prepare instance depending on create/update.

        If `create` used, create empty instance and set fields' values with
        received data. If `update` used, update existing instance with received
        data.

        """
        # Prepare instance depending on create/update
        instance = self.get_instance(attrs)

        # skip creating/updating instance related objects
        relations = self._get_relations_fields_names()

        # Set new data for instance, while ignoring relations
        for attr, value in attrs.items():
            if attr not in relations:
                setattr(instance, attr, value)

        return instance

    def validate(
        self,
        attrs: dict[str, typing.Any],
    ) -> dict[str, typing.Any]:
        """Call model's `.clean()` method during validation.

        Create:
            Just create model instance using provided data.
        Update:
            `self.instance` contains instance with new data. We apply passed
            data to it and then call `clean` method for this temp instance.

        """
        attrs = super().validate(attrs)  # type: ignore

        instance = self.prepare_instance(attrs)

        instance.clean()

        return attrs

    def _get_relations_fields_names(self) -> set[str]:
        """Extract fields with relations before validation."""
        relations = set()

        # Remove related fields from validated data for future manipulations
        for field in self.fields.values():  # type: ignore
            if field.read_only:
                continue

            if "." in field.source:
                source_attr = field.source.split(".")[0]
                relations.add(source_attr)
                continue

            is_many_model_serializer = isinstance(
                field,
                serializers.ListSerializer,
            ) and isinstance(
                field.child,
                serializers.ModelSerializer,
            )
            is_model_serializer = isinstance(
                field,
                serializers.ModelSerializer,
            )
            is_m2m_serializer = isinstance(
                field,
                serializers.ManyRelatedField,
            )
            if (
                is_many_model_serializer
                or is_model_serializer
                or is_m2m_serializer
            ):
                relations.add(field.source)

        return relations

get_instance(attrs)

Get instance depending on request.

Source code in saritasa_drf_tools/serializers/mixins/clean_validation_mixin.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_instance(
    self,
    attrs: dict[str, typing.Any],
) -> typing.Any:
    """Get instance depending on request."""
    if self.instance is not None:  # type: ignore
        # if it's update request
        return copy.deepcopy(self.instance)  # type: ignore
    model = self.Meta.model  # type: ignore
    # If attrs have `id` data, get instance form db
    # if it is a create request, we return empty instance
    if instance_id := attrs.get("id"):
        return model.objects.filter(pk=instance_id).first() or model()
    return model()

prepare_instance(attrs)

Prepare instance depending on create/update.

If create used, create empty instance and set fields' values with received data. If update used, update existing instance with received data.

Source code in saritasa_drf_tools/serializers/mixins/clean_validation_mixin.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def prepare_instance(
    self,
    attrs: dict[str, typing.Any],
) -> typing.Any:
    """Prepare instance depending on create/update.

    If `create` used, create empty instance and set fields' values with
    received data. If `update` used, update existing instance with received
    data.

    """
    # Prepare instance depending on create/update
    instance = self.get_instance(attrs)

    # skip creating/updating instance related objects
    relations = self._get_relations_fields_names()

    # Set new data for instance, while ignoring relations
    for attr, value in attrs.items():
        if attr not in relations:
            setattr(instance, attr, value)

    return instance

validate(attrs)

Call model's .clean() method during validation.

Create

Just create model instance using provided data.

Update: self.instance contains instance with new data. We apply passed data to it and then call clean method for this temp instance.

Source code in saritasa_drf_tools/serializers/mixins/clean_validation_mixin.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def validate(
    self,
    attrs: dict[str, typing.Any],
) -> dict[str, typing.Any]:
    """Call model's `.clean()` method during validation.

    Create:
        Just create model instance using provided data.
    Update:
        `self.instance` contains instance with new data. We apply passed
        data to it and then call `clean` method for this temp instance.

    """
    attrs = super().validate(attrs)  # type: ignore

    instance = self.prepare_instance(attrs)

    instance.clean()

    return attrs