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

Merge branch 'cleaning' into 'master'

Cleaning

See merge request !50
parents 0e42118e 4fe1bdf2
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):
tmp[key] = obj[key]
return tmp
raise Exception("Model not found in API configuraiton, cannot process !")