myModelSerializer.py 7.55 KB
Newer Older
Florent Chehab's avatar
Florent Chehab committed
1 2 3 4 5 6
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone

from backend_app.custom import MySerializerWithJSON
from backend_app.permissions import is_moderation_required
from backend_app.utils import get_user_level
7
from rest_framework import serializers
Florent Chehab's avatar
Florent Chehab committed
8
from rest_framework.validators import ValidationError
9
from shared.obj_moderation_permission import DEFAULT_OBJ_MODERATION_LV
Florent Chehab's avatar
Florent Chehab committed
10

Florent Chehab's avatar
Florent Chehab committed
11
from .myModel import MyModel
12
from .pendingModeration import PendingModeration
13

Florent Chehab's avatar
Florent Chehab committed
14
CLEANED_MY_MODEL_DATA = {
15 16 17 18
    "moderated_by": None,
    "moderated_on": None,
    "updated_by": None,
    "updated_on": None,
Florent Chehab's avatar
Florent Chehab committed
19 20 21
}


Florent Chehab's avatar
Florent Chehab committed
22 23 24 25
def override_data(old_data: dict, new_data: dict) -> dict:
    """Update the data in old_data with the one in new_data
    """

Florent Chehab's avatar
Florent Chehab committed
26
    for key in new_data:
Florent Chehab's avatar
Florent Chehab committed
27 28
        if key in old_data:
            old_data[key] = new_data[key]
Florent Chehab's avatar
Florent Chehab committed
29 30 31
    return old_data


Florent Chehab's avatar
Florent Chehab committed
32
class MyModelSerializer(MySerializerWithJSON):
Florent Chehab's avatar
Florent Chehab committed
33 34 35 36 37 38 39 40 41
    """Serializer to go along the MyModel Model. This serializer handles backend data moderation checks and tricks.

    Raises:
        ValidationError -- If you are trying to moderate something you don't have rights to
    """

    ######
    # Basic fields serializers
    updated_by = serializers.CharField(read_only=True)
42
    updated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
Florent Chehab's avatar
Florent Chehab committed
43

44
    moderated_by = serializers.CharField(read_only=True)
Florent Chehab's avatar
Florent Chehab committed
45 46
    moderated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)

47
    has_pending_moderation = serializers.BooleanField(read_only=True)
Florent Chehab's avatar
Florent Chehab committed
48
    model_config = serializers.SerializerMethodField()
49

50
    # For easier handling on the client side, we force an id field
Florent Chehab's avatar
Florent Chehab committed
51
    # this is useful when a model has a dedicated primary key
52 53
    id = serializers.SerializerMethodField()

Florent Chehab's avatar
Florent Chehab committed
54 55 56 57 58 59
    def get_model_config(self, obj=None) -> dict:
        """
        Serializer for the `model_config` field.
        `obj` is required in the function definition, but it's not used.
        """

Florent Chehab's avatar
Florent Chehab committed
60
        return self.Meta.model.model_config
61

Florent Chehab's avatar
Florent Chehab committed
62 63 64 65
    def get_id(self, obj: MyModel):
        """
        Serializer for the id field.
        """
66 67
        return obj.pk

68 69 70
    class Meta:
        model = MyModel

71 72 73 74 75 76
    def get_user_from_request(self):
        """
        Function to retrieve the user from the request
        """
        return self.context["request"].user

Florent Chehab's avatar
Florent Chehab committed
77
    def validate(self, attrs):
Florent Chehab's avatar
Florent Chehab committed
78
        """
Florent Chehab's avatar
Florent Chehab committed
79 80
        Validate `MyModel` fields and enforce certain field at the backend level.

Florent Chehab's avatar
Florent Chehab committed
81 82
        TODO unit test this
        """
Florent Chehab's avatar
Florent Chehab committed
83 84 85

        if "obj_moderation_level" in attrs:
            requested_obj_moder_lv = attrs["obj_moderation_level"]
86

87
            try:
88
                user_level = get_user_level(self.get_user_from_request())
89 90 91 92 93 94 95
            except KeyError:
                # if for some reason we don't have the user in the request
                # we set the level to the default one
                # this can occur during testing.
                user_level = DEFAULT_OBJ_MODERATION_LV

            if requested_obj_moder_lv > user_level:
Florent Chehab's avatar
Florent Chehab committed
96
                raise ValidationError(
97 98
                    "You can't request moderation for a higher rank than you."
                )
Florent Chehab's avatar
Florent Chehab committed
99

100
        return attrs
Florent Chehab's avatar
Florent Chehab committed
101

102
    def set_model_attr_no_moder(self, user, moderated_and_updated):
Florent Chehab's avatar
Florent Chehab committed
103 104 105 106
        """
        TODO
        TODO: rename ?
        """
107
        now = timezone.now()
108
        self.override_validated_data({"moderated_by": user, "moderated_on": now})
109 110

        if moderated_and_updated:
111
            self.override_validated_data({"updated_by": user, "updated_on": now})
112 113

    def clean_validated_data(self):
Florent Chehab's avatar
Florent Chehab committed
114 115 116
        """
        Clear fields related to update and moderation
        """
Florent Chehab's avatar
Florent Chehab committed
117
        self.override_validated_data(CLEANED_MY_MODEL_DATA)
118

Florent Chehab's avatar
Florent Chehab committed
119
    def override_validated_data(self, new_data: dict):
Florent Chehab's avatar
Florent Chehab committed
120 121 122 123 124 125
        """
        Method used to force specific attributes when saving a model
        """
        for key in new_data:
            self.validated_data[key] = new_data[key]

126
    def do_before_save(self):
Florent Chehab's avatar
Florent Chehab committed
127
        """
128
        Action to perform before saving a model
Florent Chehab's avatar
Florent Chehab committed
129
        """
Florent Chehab's avatar
Florent Chehab committed
130 131
        pass

Florent Chehab's avatar
Florent Chehab committed
132
    def save(self, *args, **kwargs):
Florent Chehab's avatar
Florent Chehab committed
133 134
        """
        Function that handles all the moderation in a smart way.
135 136
        Nothing has to be done to tell that we won't the data to be moderated,
        it is detected automatically.
Florent Chehab's avatar
Florent Chehab committed
137
        """
138 139 140 141

        user = self.context["request"].user
        user_level = get_user_level(user)

142
        self.clean_validated_data()
143
        self.do_before_save()
Florent Chehab's avatar
Florent Chehab committed
144
        ct = ContentType.objects.get_for_model(self.Meta.model)
145

146
        if is_moderation_required(
147
            self.get_model_config(), self.instance, user, user_level
148
        ):
149
            if self.instance is None:  # we need to create the main model
Florent Chehab's avatar
Florent Chehab committed
150
                # Store the user for squashing data in versions models
151
                self.validated_data.updated_by = user
Florent Chehab's avatar
Florent Chehab committed
152
                self.instance = super().save(*args, **kwargs)
153

Florent Chehab's avatar
Florent Chehab committed
154 155 156
            data_to_save = dict()
            for key in self.validated_data:
                try:
Florent Chehab's avatar
Florent Chehab committed
157 158
                    # retrieve the submitted data and save the clean json
                    # to make sure it will be savable
Florent Chehab's avatar
Florent Chehab committed
159 160 161 162 163
                    data_to_save[key] = self.initial_data[key]
                except KeyError:
                    pass

            data_to_save = override_data(data_to_save, CLEANED_MY_MODEL_DATA)
Florent Chehab's avatar
Florent Chehab committed
164

Florent Chehab's avatar
Florent Chehab committed
165
            # Save instance into pending moderation state
Florent Chehab's avatar
Florent Chehab committed
166 167 168 169
            PendingModeration.objects.update_or_create(
                content_type=ct,
                object_id=self.instance.pk,
                defaults={
170
                    "updated_on": timezone.now(),
171
                    "updated_by": user,
172 173 174
                    "new_object": data_to_save,
                },
            )
175 176 177 178

            # Performance optimization, we store the fact that there is an object pending moderation
            self.instance.has_pending_moderation = True
            self.instance.save()
179 180
            return self.instance

Florent Chehab's avatar
Florent Chehab committed
181
        else:  # Moderation is not needed, we need to check whether it's a moderation or an update with no moderation
182

183
            moderated_and_updated = True
184
            if self.instance is None:
185
                self.set_model_attr_no_moder(user, moderated_and_updated)
186
                instance = super().save(*args, **kwargs)
187
            else:
Florent Chehab's avatar
Florent Chehab committed
188 189
                try:
                    pending_instance = PendingModeration.objects.get(
190
                        content_type=ct, object_id=self.instance.pk
Florent Chehab's avatar
Florent Chehab committed
191
                    )
Florent Chehab's avatar
Florent Chehab committed
192
                    self.clean_validated_data()  # Make quez that it is done...
Florent Chehab's avatar
Florent Chehab committed
193 194 195 196 197 198 199 200
                    # We have to compare the serialized data
                    # So we make sure to compare the same elements
                    key_to_remove = []
                    for key in self.initial_data:
                        if key not in self.validated_data:
                            key_to_remove.append(key)
                    for key in key_to_remove:
                        self.initial_data.pop(key, None)
Florent Chehab's avatar
Florent Chehab committed
201

Florent Chehab's avatar
Florent Chehab committed
202
                    if pending_instance.new_object == self.initial_data:
203
                        moderated_and_updated = False
204 205
                        self.validated_data["updated_by"] = pending_instance.updated_by
                        self.validated_data["updated_on"] = pending_instance.updated_on
Florent Chehab's avatar
Florent Chehab committed
206
                    pending_instance.delete()
207

Florent Chehab's avatar
Florent Chehab committed
208 209
                except PendingModeration.DoesNotExist:
                    pass
210

211
                self.set_model_attr_no_moder(user, moderated_and_updated)
212 213 214 215 216 217
                instance = super().save(*args, **kwargs)

            # Performance optimization to know if has pending moderation
            instance.has_pending_moderation = False
            instance.save()
            return instance