Commit 4c12f242 authored by Florent Chehab's avatar Florent Chehab
Browse files

feat(backend): complete refactoring of the backend

* Added new BaseModel /serializer/viewsets
* Change names of abstract models/serializers/viewsets
* Folder structure changed in tha backend
* Added some backend test
* Corrected bug related to moderation on non versionned modeles
* Corrected bug related to no post permissions
* Updated doc accordingly

Fixes #91
parent 710a5417
Pipeline #37109 passed with stages
in 4 minutes and 54 seconds
......@@ -11,3 +11,4 @@ htmlcov
.pytest_cache
database.db
database.db-journal
.idea
......@@ -4,15 +4,18 @@ from reversion_compare.admin import CompareVersionAdmin
from backend_app.config.models import get_models
from backend_app.checks import check_classic_models, check_versionned_models
VERSIONNED_MODELS = get_models(versionned=True, requires_testing=False)
CLASSIC_MODELS = get_models(versionned=False, requires_testing=False)
# We need to register testing models, otherwise we won't be able to test properly,
# Since no migrations would privide those models.
# So don't put requires_testing=True
VERSIONED_MODELS = get_models(versionned=True) # , requires_testing=False)
CLASSIC_MODELS = get_models(versionned=False) # , requires_testing=False)
#######
# Perform some dynamic checks
#######
check_classic_models(CLASSIC_MODELS)
check_versionned_models(VERSIONNED_MODELS)
check_versionned_models(VERSIONED_MODELS)
#######
# Register the models
......@@ -22,7 +25,6 @@ for Model in CLASSIC_MODELS:
# Register the model in the admin in a standard way
admin.site.register(Model)
for Model in VERSIONNED_MODELS:
for Model in VERSIONED_MODELS:
# Register the model in the admin with versioning
admin.site.register(Model, CompareVersionAdmin)
......@@ -5,6 +5,5 @@ class BackendAppConfig(AppConfig):
name = "backend_app"
def ready(self):
import backend_app.signals.__create_user_modules_post_create # noqa:F401
import backend_app.signals.__squash_revision_by_user # noqa:F401
import backend_app.signals.__create_univ_modules_post_save # noqa:F401
import backend_app.signals.squash_revisions # noqa:F401
import backend_app.signals.auto_creation # noqa:F401
import importlib
from typing import List, Optional, Union
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
from django.conf import settings
from backend_app.models.abstract.my_model.myModelVersionned import MyModelVersionned
from .utils import load_viewsets_config
......@@ -51,9 +52,9 @@ def get_models(
Model = Viewset.serializer_class.Meta.model
if versionned is not None:
if versionned and not issubclass(Model, MyModelVersionned):
if versionned and not issubclass(Model, VersionedEssentialModule):
continue
if not versionned and issubclass(Model, MyModelVersionned):
if not versionned and issubclass(Model, VersionedEssentialModule):
continue
out.append(Model)
......
......@@ -49,7 +49,7 @@ CampusViewSet:
UserDataViewSet:
import_location: user
import_location: userData
api_end_point: userData
api_name: user-data-detail
viewset_permission: IsOwner
......@@ -66,33 +66,33 @@ CurrencyViewSet:
viewset_permission: IsStaff | ReadOnly
DepartmentViewSet:
import_location: other_core
import_location: department
api_end_point: departments
viewset_permission: IsStaff | ReadOnly
SpecialtyViewSet:
import_location: other_core
import_location: specialty
api_end_point: specialties
viewset_permission: IsStaff | ReadOnly
OfferViewSet:
import_location: other_core
import_location: offer
api_end_point: offers
viewset_permission: IsStaff | ReadOnly
CountryTaggedItemViewSet:
import_location: country
import_location: countryTaggedItem
api_end_point: countryTaggedItems
api_attr: (?P<country_id>[a-zA-Z]+)
CountryScholarshipViewSet:
import_location: country
import_location: countryScholarship
api_end_point: countryScholarships
api_attr: (?P<country_id>[a-zA-Z]+)
CountryDriViewSet:
import_location: country
import_location: countryDri
api_end_point: countryDri
api_attr: (?P<country_id>[a-zA-Z]+)
viewset_permission: IsStaff | IsDri | NoPost
......@@ -100,34 +100,34 @@ CountryDriViewSet:
CityTaggedItemViewSet:
import_location: city
import_location: cityTaggedItem
api_end_point: cityTaggedItems
api_attr: (?P<city_id>[0-9]+)
UniversityTaggedItemViewSet:
import_location: university
import_location: universityTaggedItem
api_end_point: universityTaggedItems
api_attr: (?P<univ_id>[0-9]+)
UniversityScholarshipViewSet:
import_location: university
import_location: universityScholarship
api_end_point: universityScholarships
api_attr: (?P<univ_id>[0-9]+)
UniversityInfoViewSet:
import_location: university
import_location: universityInfo
viewset_permission: IsStaff | NoPost
api_end_point: universitiesInfo
UniversitySemestersDatesViewSet:
import_location: university
import_location: universitySemestersDates
api_end_point: universitiesSemestersDates
viewset_permission: IsStaff | NoPost
UniversityDriViewSet:
import_location: university
import_location: universityDri
api_end_point: universityDri
api_attr: (?P<univ_id>[0-9]+)
viewset_permission: IsStaff | IsDri | NoPost
......@@ -135,7 +135,7 @@ UniversityDriViewSet:
CampusTaggedItemViewSet:
import_location: campus
import_location: campusTaggedItem
api_end_point: campusTaggedItems
api_attr: (?P<campus_id>[0-9]+)
......@@ -147,50 +147,50 @@ MainCampusViewSet:
RecommendationViewSet:
import_location: user
import_location: recommendation
api_end_point: userRecommendations
RecommendationListViewSet:
import_location: user
import_location: recommendationList
api_end_point: userRecommendationLists
PreviousDepartureViewSet:
import_location: user
import_location: previousDeparture
api_end_point: universitiesPreviousDepartures
viewset_permission: ReadOnly
PreviousDepartureFeedbackViewSet:
import_location: user
import_location: previousDepartureFeedback
api_end_point: universitiesPreviousDepartureFeedback
PendingModerationViewSet:
import_location: abstract.my_model
import_location: pendingModeration
api_end_point: pendingModeration
viewset_permission: IsStaff
VersionViewSet:
import_location: abstract.my_model
import_location: version
api_end_point: versions
api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+)
api_name: versionsList
viewset_permission: IsStaff | ReadOnly
PendingModerationObjViewSet:
import_location: abstract.my_model
import_location: pendingModeration
api_end_point: pendingModerationObj
api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+)
api_name: pendingModerationObj
viewset_permission: ReadOnly
ForTestingModerationViewSet:
import_location: abstract.my_model
import_location: for_testing.moderation
api_end_point: test/moderation
requires_testing: true
ForTestingVersioningViewSet:
import_location: abstract.my_model
import_location: for_testing.versioning
api_end_point: test/versioning
requires_testing: true
from .mySerializerWithJSON import MySerializerWithJSON
__all__ = ["MySerializerWithJSON"]
......@@ -2,7 +2,7 @@ from base_app.models import User
from django.utils import timezone
import reversion
from backend_app.models.abstract.my_model import MyModel
from backend_app.models.abstract.essentialModule import EssentialModule
class LoadGeneric(object):
......@@ -10,7 +10,7 @@ class LoadGeneric(object):
"""
@classmethod
def add_info_and_save(cls, obj: MyModel, admin: User):
def add_info_and_save(cls, obj: EssentialModule, admin: User):
with reversion.create_revision():
obj.moderated_by = admin
obj.updated_by = admin
......
......@@ -2,16 +2,15 @@ from datetime import datetime
from base_app.models import User
from backend_app.models.country import Country, CountryScholarship
from backend_app.models.country import Country
from backend_app.models.countryScholarship import CountryScholarship
from backend_app.models.currency import Currency
from backend_app.models.tag import Tag
from backend_app.models.university import (
University,
UniversityDri,
UniversityInfo,
UniversitySemestersDates,
UniversityTaggedItem,
)
from backend_app.models.university import University
from backend_app.models.universityDri import UniversityDri
from backend_app.models.universityInfo import UniversityInfo
from backend_app.models.universitySemestersDates import UniversitySemestersDates
from backend_app.models.universityTaggedItem import UniversityTaggedItem
from .loadGeneric import LoadGeneric
......
# Generated by Django 2.1.7 on 2019-03-16 11:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("backend_app", "0001_new-initial")]
operations = [
migrations.RenameField(
model_name="countryscholarship",
old_name="type",
new_name="short_description",
),
migrations.RenameField(
model_name="universityscholarship",
old_name="type",
new_name="short_description",
),
]
SEMESTER_OPTIONS = (("a", "autumn"), ("p", "spring"))
__all__ = ["SEMESTER_OPTION"]
__all__ = ["SEMESTER_OPTIONS"]
from django.db import models
from backend_app.custom.mySerializerWithJSON import MySerializerWithJSON
from backend_app.permissions.viewsets import get_viewset_permissions
from rest_framework import serializers, viewsets
class BaseModel(models.Model):
"""
All models in the app inherits from this one.
As of now, this model doesn't have any special fields.
It is basically here to have a coherent naming convention. In fact some
high level behaviors have been implemented in the corresponding Serializer and
viewset.
"""
class Meta:
abstract = True
class BaseModelSerializer(MySerializerWithJSON):
"""
Serializer to go along the BaseModel model. This serializer make sure some
relevant data is always returned.
"""
obj_info = serializers.SerializerMethodField()
# For easier handling on the client side, we force an id field
# this is useful when a model has a dedicated primary key
id = serializers.SerializerMethodField()
def get_obj_info(self, obj) -> dict:
"""
Serializer for the `obj_info` *dynamic* field.
`obj` is required in the function definition, but it's not used.
For all object return by the backend api, we add a custom `obj_info`
field. The default value are chown below.
This methods is overrided in EssentialModuleSerializer for
a smarter behavior.
"""
return {"user_can_edit": False, "user_can_moderate": False}
def get_id(self, obj: BaseModel):
"""
Serializer for the id field.
"""
return obj.pk
class Meta:
model = BaseModel
class BaseModelViewSet(viewsets.ModelViewSet):
"""
Custom default viewset
"""
serializer_class = BaseModelSerializer
_permission_classes_cached = None
@property
def permission_classes(self):
"""
We retrieve permissions classes for the viewsets
From the config file and cache it.
"""
if self._permission_classes_cached is None:
self._permission_classes_cached = get_viewset_permissions(
self.__class__.__name__
)
return self._permission_classes_cached
from .basicModule import BasicModule, BasicModuleSerializer, BasicModuleViewSet
__all__ = ["BasicModule", "BasicModuleSerializer", "BasicModuleViewSet"]
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from rest_framework import serializers
from backend_app.custom import MySerializerWithJSON
from backend_app.permissions import is_moderation_required
from backend_app.permissions.moderation import is_moderation_required
from backend_app.permissions.utils import Request as FakeRequest
from backend_app.utils import get_user_level
from rest_framework import serializers
from rest_framework.validators import ValidationError
from backend_app.config.other import DEFAULT_OBJ_MODERATION_LV
from base_app.models import User
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from backend_app.config.other import (
DEFAULT_OBJ_MODERATION_LV,
OBJ_MODERATION_PERMISSIONS,
)
from backend_app.models.pendingModeration import PendingModeration
from .base import BaseModel, BaseModelSerializer, BaseModelViewSet
POSSIBLE_OBJ_MODER_LV = [
OBJ_MODERATION_PERMISSIONS[key] for key in OBJ_MODERATION_PERMISSIONS
]
def validate_obj_model_lv(value):
if value not in POSSIBLE_OBJ_MODER_LV:
raise ValidationError("obj_moderation_level not recognized")
#
#
#
#
#
#
# Module
#
#
#
#
#
#
#
#
class EssentialModule(BaseModel):
"""
All models in the app deppend of this one.
It contains the required attributes for managing optional data moderation.
All the logic behind moderation is done in EssentialModuleSerializer
"""
# store the update author
updated_by = models.ForeignKey(
User, null=True, on_delete=models.SET_NULL, related_name="+"
)
# store the update date (model can be updated without moderation)
updated_on = models.DateTimeField(null=True)
# store the moderator
moderated_by = models.ForeignKey(
User, null=True, on_delete=models.SET_NULL, related_name="+"
)
# store the moderation date
moderated_on = models.DateTimeField(null=True)
from .myModel import MyModel
from .pendingModeration import PendingModeration
# Store the object moderation level by default
obj_moderation_level = models.SmallIntegerField(
default=DEFAULT_OBJ_MODERATION_LV,
validators=[MinValueValidator(0), validate_obj_model_lv],
)
# Add the link to pending moderation
pending_moderation = GenericRelation(PendingModeration)
# A bit of optimization: we store if there is something pending moderation
has_pending_moderation = models.BooleanField(default=False)
CLEANED_MY_MODEL_DATA = {
class Meta:
abstract = True
#
#
#
#
#
#
# Serializer
#
#
#
#
#
#
#
#
CLEANED_ESSENTIAL_MODULE_MODEL_DATA = {
"moderated_by": None,
"moderated_on": None,
"updated_by": None,
......@@ -29,8 +118,8 @@ def override_data(old_data: dict, new_data: dict) -> dict:
return old_data
class MyModelSerializer(MySerializerWithJSON):
"""Serializer to go along the MyModel Model. This serializer handles backend data moderation checks and tricks.
class EssentialModuleSerializer(BaseModelSerializer):
"""Serializer to go along the EssentialModule 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
......@@ -45,21 +134,28 @@ class MyModelSerializer(MySerializerWithJSON):
moderated_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
has_pending_moderation = serializers.BooleanField(read_only=True)
obj_info = serializers.SerializerMethodField()
# For easier handling on the client side, we force an id field
# this is useful when a model has a dedicated primary key
id = serializers.SerializerMethodField()
# Add a content_type_id field to be able to find versions
content_type_id = serializers.SerializerMethodField()
def get_content_type_id(self, obj):
"""
Serializer for content type
"""
return ContentType.objects.get_for_model(self.Meta.model).id
def get_obj_info(self, obj) -> dict:
"""
Serializer for the `obj_info` *dynamic* field.
`obj` is required in the function definition, but it's not used.
"""
try:
user_can_edit = self.context["user_can_edit"]
except KeyError:
# Some viewsets don't inherit from MyModelViewset and therefore
# In case some viewsets don't inherit from BaseModelViewSet and therefore
# don't have the method to produce context["user_can_edit"]
# Anyway, those Viewsets should be readonly, so we can return false.
user_can_edit = False
......@@ -71,14 +167,8 @@ class MyModelSerializer(MySerializerWithJSON):
),
}
def get_id(self, obj: MyModel):
"""
Serializer for the id field.
"""
return obj.pk
class Meta:
model = MyModel
model = EssentialModule
def get_user_from_request(self):
"""
......@@ -88,11 +178,10 @@ class MyModelSerializer(MySerializerWithJSON):
def validate(self, attrs):
"""
Validate `MyModel` fields and enforce certain field at the backend level.
Validate `BaseModel` fields and enforce certain field at the backend level.
Checks that the requested moderation level is not higher than the one of the user.
"""
if "obj_moderation_level" in attrs:
requested_obj_moder_lv = attrs["obj_moderation_level"]
......@@ -130,7 +219,7 @@ class MyModelSerializer(MySerializerWithJSON):
"""
Clear fields related to update and moderation
"""
self.override_validated_data(CLEANED_MY_MODEL_DATA)
self.override_validated_data(CLEANED_ESSENTIAL_MODULE_MODEL_DATA)
def override_validated_data(self, new_data: dict):
"""
......@@ -174,7 +263,9 @@ class MyModelSerializer(MySerializerWithJSON):
except KeyError:
pass
data_to_save = override_data(data_to_save, CLEANED_MY_MODEL_DATA)
data_to_save = override_data(
data_to_save, CLEANED_ESSENTIAL_MODULE_MODEL_DATA
)
# Save instance into pending moderation state
PendingModeration.objects.update_or_create(
......@@ -233,3 +324,62 @@ class MyModelSerializer(MySerializerWithJSON):
instance.has_pending_moderation = False
instance.save()
return instance
#
#
#