myModelSerializer.py 7.97 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
        Checks that the requested moderation level is not higher than the one of the user.
Florent Chehab's avatar
Florent Chehab committed
82
        """
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

Florent Chehab's avatar
Florent Chehab committed
102
103
104
    def set_model_attrs_for_moderation_and_update(
        self, user, moderated_and_updated: bool
    ):
Florent Chehab's avatar
Florent Chehab committed
105
        """
Florent Chehab's avatar
Florent Chehab committed
106
107
108
109
        Overrides model attributes regarding moderation and update.
        The moderated field is set to the request user. The moderated_on field is reset to now.

        If there was an updated, the updated_by field and updated_on field are also reset.
Florent Chehab's avatar
Florent Chehab committed
110
        """
111
        now = timezone.now()
112
        self.override_validated_data({"moderated_by": user, "moderated_on": now})
113
114

        if moderated_and_updated:
115
            self.override_validated_data({"updated_by": user, "updated_on": now})
116
117

    def clean_validated_data(self):
Florent Chehab's avatar
Florent Chehab committed
118
119
120
        """
        Clear fields related to update and moderation
        """
Florent Chehab's avatar
Florent Chehab committed
121
        self.override_validated_data(CLEANED_MY_MODEL_DATA)
122

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

130
    def do_before_save(self):
Florent Chehab's avatar
Florent Chehab committed
131
        """
132
        Action to perform before saving a model
Florent Chehab's avatar
Florent Chehab committed
133
        """
Florent Chehab's avatar
Florent Chehab committed
134
135
        pass

Florent Chehab's avatar
Florent Chehab committed
136
    def save(self, *args, **kwargs):
Florent Chehab's avatar
Florent Chehab committed
137
138
        """
        Function that handles all the moderation in a smart way.
139
140
        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
141
        """
142
143
144
145

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

146
        self.clean_validated_data()
147
        self.do_before_save()
Florent Chehab's avatar
Florent Chehab committed
148
        ct = ContentType.objects.get_for_model(self.Meta.model)
149

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

Florent Chehab's avatar
Florent Chehab committed
158
159
160
            data_to_save = dict()
            for key in self.validated_data:
                try:
Florent Chehab's avatar
Florent Chehab committed
161
162
                    # retrieve the submitted data and save the clean json
                    # to make sure it will be savable
Florent Chehab's avatar
Florent Chehab committed
163
164
165
166
167
                    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
168

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

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

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

187
            moderated_and_updated = True
188
            if self.instance is None:
Florent Chehab's avatar
Florent Chehab committed
189
190
191
                self.set_model_attrs_for_moderation_and_update(
                    user, moderated_and_updated
                )
192
                instance = super().save(*args, **kwargs)
193
            else:
Florent Chehab's avatar
Florent Chehab committed
194
195
                try:
                    pending_instance = PendingModeration.objects.get(
196
                        content_type=ct, object_id=self.instance.pk
Florent Chehab's avatar
Florent Chehab committed
197
                    )
Florent Chehab's avatar
Florent Chehab committed
198
                    self.clean_validated_data()  # Make quez that it is done...
Florent Chehab's avatar
Florent Chehab committed
199
200
201
202
203
204
205
206
                    # 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
207

Florent Chehab's avatar
Florent Chehab committed
208
                    if pending_instance.new_object == self.initial_data:
209
                        moderated_and_updated = False
210
211
                        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
212
                    pending_instance.delete()
213

Florent Chehab's avatar
Florent Chehab committed
214
215
                except PendingModeration.DoesNotExist:
                    pass
216

Florent Chehab's avatar
Florent Chehab committed
217
218
219
                self.set_model_attrs_for_moderation_and_update(
                    user, moderated_and_updated
                )
220
221
222
223
224
225
                instance = super().save(*args, **kwargs)

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