Commit 7266140c authored by Florent Chehab's avatar Florent Chehab
Browse files

Merge branch 'cleaning' into 'master'

Cleaning

See merge request rex-dri/rex-dri!50
parents 0e42118e 4fe1bdf2
Pipeline #35396 passed with stages
in 4 minutes and 15 seconds
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
from rest_framework import serializers
from rest_framework.validators import ValidationError
from django.utils import timezone
from .pendingModeration import PendingModeration
from django.contrib.contenttypes.models import ContentType
from .myModel import MyModel
from .pendingModeration import PendingModerationSerializer
from backend_app.utils import get_user_level
from backend_app.permissions import is_moderation_required
from backend_app.custom import MySerializerWithJSON
from .pendingModeration import PendingModeration, PendingModerationSerializer
CLEANED_MY_MODEL_DATA = {
"moderated_by": None,
......@@ -17,7 +18,10 @@ CLEANED_MY_MODEL_DATA = {
}
def override_data(old_data, new_data):
def override_data(old_data: dict, new_data: dict) -> dict:
"""Update the data in old_data with the one in new_data
"""
for key in new_data:
if key in old_data:
old_data[key] = new_data[key]
......@@ -25,10 +29,20 @@ def override_data(old_data, new_data):
class MyModelSerializer(MySerializerWithJSON):
moderated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
"""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)
updated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
moderated_by = serializers.CharField(read_only=True)
updated_by = serializers.CharField(read_only=True)
moderated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
pending_moderation = serializers.SerializerMethodField()
model_config = serializers.SerializerMethodField()
......@@ -36,12 +50,24 @@ class MyModelSerializer(MySerializerWithJSON):
# this is useful when a model has a dedicated primary key
id = serializers.SerializerMethodField()
def get_model_config(self, obj=None):
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.
"""
return self.Meta.model.model_config
# Optimization
# Class attribute to force the pending moderation to return the pending moderation data
# Or versionned element to return the number of versions
FORCE_FULL_DISPLAY = False
def get_pending_moderation(self, obj):
def get_pending_moderation(self, obj: MyModel):
"""
Serializer for the `pending_moderation` field
"""
# Optimization, in list mode, fetching all the pending moderation information is not optimal at all
if not self.FORCE_FULL_DISPLAY and self.context["view"].action == "list":
return None
else:
......@@ -53,19 +79,28 @@ class MyModelSerializer(MySerializerWithJSON):
pending, many=True, read_only=True, context=self.context
).data
def get_id(self, obj):
def get_id(self, obj: MyModel):
"""
Serializer for the id field.
"""
return obj.pk
class Meta:
model = MyModel
def my_validate(self, attrs):
"""
Function to be redefined in the subclasses to validate class fields.
"""
return attrs
def validate(self, attrs):
"""
Validate `MyModel` fields and enforce certain field at the backend level.
TODO unit test this
"""
# Enforce fields values based on request
self.user = self.context["request"].user
self.user_level = get_user_level(self.user)
......@@ -80,6 +115,10 @@ class MyModelSerializer(MySerializerWithJSON):
return self.my_validate(attrs)
def set_model_attr_no_moder(self, moderated_and_updated):
"""
TODO
TODO: rename ?
"""
now = timezone.now()
self.override_validated_data({"moderated_by": self.user, "moderated_on": now})
......@@ -87,9 +126,12 @@ class MyModelSerializer(MySerializerWithJSON):
self.override_validated_data({"updated_by": self.user, "updated_on": now})
def clean_validated_data(self):
"""
Clear fields related to update and moderation
"""
self.override_validated_data(CLEANED_MY_MODEL_DATA)
def override_validated_data(self, new_data):
def override_validated_data(self, new_data: dict):
"""
Method used to force specific attributes when saving a model
"""
......@@ -97,12 +139,23 @@ class MyModelSerializer(MySerializerWithJSON):
self.validated_data[key] = new_data[key]
def my_pre_save(self):
"""
TODO, Analyse if usefull
"""
pass
def save(self, *args, **kwargs):
"""
TODO analyse, usefull ?
"""
return self.my_save(*args, **kwargs)
def my_save(self, *args, **kwargs):
"""
Function that handles all the moderation in a smart way.
Nothing has to be done to tell that we won't the data to be moderated, it is detected automatically.
"""
self.clean_validated_data()
self.my_pre_save()
ct = ContentType.objects.get_for_model(self.Meta.model)
......@@ -113,7 +166,7 @@ class MyModelSerializer(MySerializerWithJSON):
if self.instance is None: # we need to create the main model
# Store the user for squashing data in versions models
self.validated_data.updated_by = self.user
self.instance = super(MyModelSerializer, self).save(*args, **kwargs)
self.instance = super().save(*args, **kwargs)
data_to_save = dict()
for key in self.validated_data:
......@@ -126,6 +179,7 @@ class MyModelSerializer(MySerializerWithJSON):
data_to_save = override_data(data_to_save, CLEANED_MY_MODEL_DATA)
# Save instance into pending moderation state
PendingModeration.objects.update_or_create(
content_type=ct,
object_id=self.instance.pk,
......@@ -137,18 +191,18 @@ class MyModelSerializer(MySerializerWithJSON):
)
return self.instance
else:
else: # Moderation is not needed, we need to check whether it's a moderation or an update with no moderation
moderated_and_updated = True
if self.instance is None:
self.set_model_attr_no_moder(moderated_and_updated)
return super(MyModelSerializer, self).save(*args, **kwargs)
return super().save(*args, **kwargs)
else:
try:
pending_instance = PendingModeration.objects.get(
content_type=ct, object_id=self.instance.pk
)
self.clean_validated_data() # Make that it is done...
self.clean_validated_data() # Make quez that it is done...
# We have to compare the serialized data
# So we make sure to compare the same elements
key_to_remove = []
......@@ -157,6 +211,7 @@ class MyModelSerializer(MySerializerWithJSON):
key_to_remove.append(key)
for key in key_to_remove:
self.initial_data.pop(key, None)
if pending_instance.new_object == self.initial_data:
moderated_and_updated = False
self.validated_data["updated_by"] = pending_instance.updated_by
......@@ -167,4 +222,4 @@ class MyModelSerializer(MySerializerWithJSON):
pass
self.set_model_attr_no_moder(moderated_and_updated)
return super(MyModelSerializer, self).save(*args, **kwargs)
return super().save(*args, **kwargs)
from django.contrib.contenttypes.models import ContentType
from django.core import serializers as djangoSerializers
from django.core.serializers.base import DeserializationError
import reversion
from backend_app.custom import MySerializerWithJSON
from backend_app.models.abstract.my_model import (
MyModel,
MyModelSerializer,
MyModelViewSet,
)
from backend_app.signals.__squash_revision_by_user import new_revision_saved
from rest_framework import serializers, mixins, viewsets
import reversion
from reversion.models import Version
from django.contrib.contenttypes.models import ContentType
from django.core.serializers.base import DeserializationError
from django.core import serializers as djangoSerializers
from backend_app.utils import get_viewset_permissions
from backend_app.custom import MySerializerWithJSON
from rest_framework import mixins, serializers, viewsets
from reversion.models import Version
class MyModelVersionned(MyModel):
"""
Custom MyModel that will be versionned in the app
"""
@classmethod
def get_serializer(cls):
"""
......@@ -29,19 +34,36 @@ class MyModelVersionned(MyModel):
class MyModelVersionnedSerializer(MyModelSerializer):
"""
Serializer for versionned models
"""
# Add a nb_versions field
nb_versions = serializers.SerializerMethodField()
# Add a content_type_id field to be able to find versions
content_type_id = serializers.SerializerMethodField()
def get_nb_versions(self, obj):
"""
Serializer for the nb_version field
With a bit of optimization
"""
if self.FORCE_FULL_DISPLAY or self.context["view"].action != "list":
versions = Version.objects.get_for_object(obj)
return len(versions)
return None
def get_content_type_id(self, obj):
"""
Serializer for content type
"""
return ContentType.objects.get_for_model(self.Meta.model).id
def save(self, *args, **kwargs):
"""
Custom save function to use reversion.
"""
with reversion.create_revision():
res = self.my_save(*args, **kwargs)
reversion.set_user(res.updated_by)
......@@ -50,19 +72,29 @@ class MyModelVersionnedSerializer(MyModelSerializer):
class MyModelVersionnedViewSet(MyModelViewSet):
"""
Viewset for the versionned models
"""
serializer_class = MyModelVersionnedSerializer
class VersionSerializer(MySerializerWithJSON):
"""
Custom serializer for the (reversion) version model
"""
data = serializers.SerializerMethodField()
def get_data(self, obj):
"""
Serilizer for the data field
TODO test
"""
data = obj.serialized_data
try:
# We try to deserialize the version
tmp = list(
djangoSerializers.deserialize(obj.format, data, ignorenonexistent=True)
)[0]
......@@ -81,9 +113,12 @@ class VersionSerializer(MySerializerWithJSON):
fields = ("data", "id")
class VersionViewSet(
mixins.ListModelMixin, viewsets.GenericViewSet
): # TODO better presentation
class VersionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
Viewset for the versions
TODO better presentation
"""
permission_classes = get_viewset_permissions("VersionViewSet")
serializer_class = VersionSerializer
......
from .myModelSerializer import MyModelSerializer
from backend_app.permissions import DEFAULT_VIEWSET_PERMISSIONS
from rest_framework import viewsets
from .myModelSerializer import MyModelSerializer
class MyModelViewSet(viewsets.ModelViewSet):
"""
Custom default viewset
"""
serializer_class = MyModelSerializer
permission_classes = DEFAULT_VIEWSET_PERMISSIONS
# Class attribute to tell that return 1 element and not a list of element
# For example when querying userData, we don't specify the userId and query the viewset
# in list mode; still we want to return only one
LIST_SHOULD_BE_DETAIL = False
def list(self, request, *args, **kwargs):
response = super(viewsets.ModelViewSet, self).list( # pylint: disable=E1003
request, *args, **kwargs
) # call the original 'list'
"""
Extend the list function of the viewset class
"""
response = super().list(request, *args, **kwargs) # call the original 'list'
if self.LIST_SHOULD_BE_DETAIL:
if len(response.data) == 0:
......
from django.db import models
from rest_framework import serializers, viewsets
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from backend_app.fields import JSONField
from django.contrib.auth.models import User
from backend_app.utils import get_viewset_permissions, get_model_config
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from backend_app.custom import MySerializerWithJSON
from backend_app.fields import JSONField
from backend_app.utils import get_model_config, get_viewset_permissions
from rest_framework import serializers, viewsets
class PendingModeration(models.Model):
"""
Model to hold models that are pending moderation.
"""
model_config = get_model_config("PendingModeration")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
......@@ -18,6 +23,7 @@ class PendingModeration(models.Model):
updated_by = models.ForeignKey(User, on_delete=models.CASCADE)
updated_on = models.DateTimeField(null=True)
# Object pending moderation
new_object = JSONField(default=dict)
class Meta:
......@@ -25,6 +31,10 @@ class PendingModeration(models.Model):
class PendingModerationSerializer(MySerializerWithJSON):
"""
Serializer for the Pending Moderation Model
"""
content_type = serializers.CharField(read_only=True)
object_id = serializers.CharField(read_only=True)
......@@ -39,6 +49,10 @@ class PendingModerationSerializer(MySerializerWithJSON):
class PendingModerationViewSet(viewsets.ModelViewSet):
"""
Viewset for the pending moderation model.
"""
permission_classes = get_viewset_permissions("PendingModerationViewSet")
queryset = PendingModeration.objects.all() # pylint: disable=E1101
serializer_class = PendingModerationSerializer
from django.core.validators import MinValueValidator
from django.db import models
from backend_app.models.abstract.basic_module import (
BasicModule,
BasicModuleSerializer,
......@@ -6,7 +8,6 @@ from backend_app.models.abstract.basic_module import (
)
from backend_app.models.currency import Currency
from rest_framework import serializers
from django.core.validators import MinValueValidator
SCHOLARSHIP_FREQUENCIES = (
("w", "week"),
......@@ -18,7 +19,11 @@ SCHOLARSHIP_FREQUENCIES = (
class Scholarship(BasicModule):
"""
Abstract model for scholarships
"""
# TODO change this, don't use python primitive
type = models.CharField(max_length=200)
currency = models.ForeignKey(Currency, null=True, on_delete=models.PROTECT)
other_advantages = models.CharField(default="", blank=True, max_length=5000)
......@@ -44,9 +49,16 @@ class Scholarship(BasicModule):
class ScholarshipSerializer(BasicModuleSerializer):
"""
Serializer for the scholarship class
"""
FORCE_FULL_DISPLAY = True
def my_validate(self, attrs):
"""
Custom attribute validation
"""
attrs = super(ScholarshipSerializer, self).my_validate(attrs)
if attrs["amount_min"] is not None:
if attrs["currency"] is None:
......
from django.db import models
from backend_app.fields import JSONField
from backend_app.models.abstract.basic_module import (
BasicModule,
BasicModuleSerializer,
......@@ -6,10 +8,13 @@ from backend_app.models.abstract.basic_module import (
)
from backend_app.models.tag import Tag
from backend_app.validators.tag import tagged_item_validation
from backend_app.fields import JSONField
class TaggedItem(BasicModule):
"""
Abstract model to represent a tagged item
"""
tag = models.ForeignKey(Tag, related_name="+", on_delete=models.PROTECT)
custom_content = JSONField(default=dict)
......@@ -18,6 +23,10 @@ class TaggedItem(BasicModule):
class TaggedItemSerializer(BasicModuleSerializer):
"""
Serializer for tagged items
"""
FORCE_FULL_DISPLAY = True
def my_validate(self, attrs):
......@@ -26,5 +35,12 @@ class TaggedItemSerializer(BasicModuleSerializer):
class TaggedItemViewSet(BasicModuleViewSet):
"""
Tagged item viewset
"""
def extend_queryset(self):
"""
Extend the queryset for a bit of optimization
"""
return self.my_model_queryset.prefetch_related("tag")
......@@ -18,65 +18,54 @@ router = routers.DefaultRouter()
ALL_MODELS = []
ALL_VIEWSETS = []
# Automatically loading models based on API config file
# Automatically load models and viewset based on API config file
api_config = get_api_config()
for model in api_config:
model = DotMap(model)
if not model.requires_testing:
for entry in api_config:
model_obj = DotMap(entry)
if (not model_obj.requires_testing) or (
settings.TESTING and model_obj.requires_testing
):
module = importlib.import_module(
"backend_app.models.{}".format(model.import_location)
"backend_app.models.{}".format(model_obj.import_location)
)
Viewset = getattr(module, model.viewset)
Viewset = getattr(module, model_obj.viewset)
ALL_VIEWSETS.append(Viewset)
if model.model is not None and not model.ignore_in_admin:
ALL_MODELS.append(getattr(module, model.model))
# print(viewset)
str_url = model.api_end_point
if "api_attr" in model:
str_url += "/{}".format(model.api_attr)
if "api_name" in model:
router.register(str_url, Viewset, model.api_name)
if model_obj.model is not None and not model_obj.ignore_in_admin:
ALL_MODELS.append(getattr(module, model_obj.model))
# Creating the correct router entry
str_url = model_obj.api_end_point
if "api_attr" in model_obj:
str_url += "/{}".format(model_obj.api_attr)
if "api_name" in model_obj:
router.register(str_url, Viewset, model_obj.api_name)
else:
router.register(str_url, Viewset)
if settings.TESTING:
for model in api_config:
model = DotMap(model)
if model.requires_testing:
module = importlib.import_module(
"backend_app.models.{}".format(model.import_location)
)
Viewset = getattr(module, model.viewset)
ALL_VIEWSETS.append(Viewset)
if model.model is not None:
ALL_MODELS.append(getattr(module, model.model))
str_url = model.api_end_point
if "api_attr" in model:
str_url += "/{}".format(model.api_attr)
if "api_name" in model:
router.register(str_url, Viewset, model.api_name)
else:
router.register(str_url, Viewset, model.viewset)
# Add all the endpoints for the base api
urlpatterns += [url(r"^api/", include(router.urls))]
# Add some custom APIs
urlpatterns.append(path("api/serverModerationStatus/", views.app_moderation_status))
#######
# Models and Viewset checks
#######
# Check that all the models config have been set
for Model in ALL_MODELS:
for key in Model.model_config:
val = Model.model_config[key]
if val is None:
raise Exception(
"You forgot to set the {} config variable in the model {}".format(
key, str(model)
key, str(Model)
)
)
# Check that all the viewsets have at least the permissions from the default viewset permissions
for Viewset in ALL_VIEWSETS:
for p in DEFAULT_VIEWSET_PERMISSIONS:
v_p = Viewset.permission_classes
......
......@@ -16,4 +16,4 @@ def get_model_config(model):