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 ...@@ -11,3 +11,4 @@ htmlcov
.pytest_cache .pytest_cache
database.db database.db
database.db-journal database.db-journal
.idea
...@@ -4,15 +4,18 @@ from reversion_compare.admin import CompareVersionAdmin ...@@ -4,15 +4,18 @@ from reversion_compare.admin import CompareVersionAdmin
from backend_app.config.models import get_models from backend_app.config.models import get_models
from backend_app.checks import check_classic_models, check_versionned_models from backend_app.checks import check_classic_models, check_versionned_models
VERSIONNED_MODELS = get_models(versionned=True, requires_testing=False) # We need to register testing models, otherwise we won't be able to test properly,
CLASSIC_MODELS = get_models(versionned=False, requires_testing=False) # 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 # Perform some dynamic checks
####### #######
check_classic_models(CLASSIC_MODELS) check_classic_models(CLASSIC_MODELS)
check_versionned_models(VERSIONNED_MODELS) check_versionned_models(VERSIONED_MODELS)
####### #######
# Register the models # Register the models
...@@ -22,7 +25,6 @@ for Model in CLASSIC_MODELS: ...@@ -22,7 +25,6 @@ for Model in CLASSIC_MODELS:
# Register the model in the admin in a standard way # Register the model in the admin in a standard way
admin.site.register(Model) admin.site.register(Model)
for Model in VERSIONED_MODELS:
for Model in VERSIONNED_MODELS:
# Register the model in the admin with versioning # Register the model in the admin with versioning
admin.site.register(Model, CompareVersionAdmin) admin.site.register(Model, CompareVersionAdmin)
...@@ -5,6 +5,5 @@ class BackendAppConfig(AppConfig): ...@@ -5,6 +5,5 @@ class BackendAppConfig(AppConfig):
name = "backend_app" name = "backend_app"
def ready(self): def ready(self):
import backend_app.signals.__create_user_modules_post_create # noqa:F401 import backend_app.signals.squash_revisions # noqa:F401
import backend_app.signals.__squash_revision_by_user # noqa:F401 import backend_app.signals.auto_creation # noqa:F401
import backend_app.signals.__create_univ_modules_post_save # noqa:F401
import importlib import importlib
from typing import List, Optional, Union from typing import List, Optional, Union
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
from django.conf import settings from django.conf import settings
from backend_app.models.abstract.my_model.myModelVersionned import MyModelVersionned
from .utils import load_viewsets_config from .utils import load_viewsets_config
...@@ -51,9 +52,9 @@ def get_models( ...@@ -51,9 +52,9 @@ def get_models(
Model = Viewset.serializer_class.Meta.model Model = Viewset.serializer_class.Meta.model
if versionned is not None: if versionned is not None:
if versionned and not issubclass(Model, MyModelVersionned): if versionned and not issubclass(Model, VersionedEssentialModule):
continue continue
if not versionned and issubclass(Model, MyModelVersionned): if not versionned and issubclass(Model, VersionedEssentialModule):
continue continue
out.append(Model) out.append(Model)
......
...@@ -49,7 +49,7 @@ CampusViewSet: ...@@ -49,7 +49,7 @@ CampusViewSet:
UserDataViewSet: UserDataViewSet:
import_location: user import_location: userData
api_end_point: userData api_end_point: userData
api_name: user-data-detail api_name: user-data-detail
viewset_permission: IsOwner viewset_permission: IsOwner
...@@ -66,33 +66,33 @@ CurrencyViewSet: ...@@ -66,33 +66,33 @@ CurrencyViewSet:
viewset_permission: IsStaff | ReadOnly viewset_permission: IsStaff | ReadOnly
DepartmentViewSet: DepartmentViewSet:
import_location: other_core import_location: department
api_end_point: departments api_end_point: departments
viewset_permission: IsStaff | ReadOnly viewset_permission: IsStaff | ReadOnly
SpecialtyViewSet: SpecialtyViewSet:
import_location: other_core import_location: specialty
api_end_point: specialties api_end_point: specialties
viewset_permission: IsStaff | ReadOnly viewset_permission: IsStaff | ReadOnly
OfferViewSet: OfferViewSet:
import_location: other_core import_location: offer
api_end_point: offers api_end_point: offers
viewset_permission: IsStaff | ReadOnly viewset_permission: IsStaff | ReadOnly
CountryTaggedItemViewSet: CountryTaggedItemViewSet:
import_location: country import_location: countryTaggedItem
api_end_point: countryTaggedItems api_end_point: countryTaggedItems
api_attr: (?P<country_id>[a-zA-Z]+) api_attr: (?P<country_id>[a-zA-Z]+)
CountryScholarshipViewSet: CountryScholarshipViewSet:
import_location: country import_location: countryScholarship
api_end_point: countryScholarships api_end_point: countryScholarships
api_attr: (?P<country_id>[a-zA-Z]+) api_attr: (?P<country_id>[a-zA-Z]+)
CountryDriViewSet: CountryDriViewSet:
import_location: country import_location: countryDri
api_end_point: countryDri api_end_point: countryDri
api_attr: (?P<country_id>[a-zA-Z]+) api_attr: (?P<country_id>[a-zA-Z]+)
viewset_permission: IsStaff | IsDri | NoPost viewset_permission: IsStaff | IsDri | NoPost
...@@ -100,34 +100,34 @@ CountryDriViewSet: ...@@ -100,34 +100,34 @@ CountryDriViewSet:
CityTaggedItemViewSet: CityTaggedItemViewSet:
import_location: city import_location: cityTaggedItem
api_end_point: cityTaggedItems api_end_point: cityTaggedItems
api_attr: (?P<city_id>[0-9]+) api_attr: (?P<city_id>[0-9]+)
UniversityTaggedItemViewSet: UniversityTaggedItemViewSet:
import_location: university import_location: universityTaggedItem
api_end_point: universityTaggedItems api_end_point: universityTaggedItems
api_attr: (?P<univ_id>[0-9]+) api_attr: (?P<univ_id>[0-9]+)
UniversityScholarshipViewSet: UniversityScholarshipViewSet:
import_location: university import_location: universityScholarship
api_end_point: universityScholarships api_end_point: universityScholarships
api_attr: (?P<univ_id>[0-9]+) api_attr: (?P<univ_id>[0-9]+)
UniversityInfoViewSet: UniversityInfoViewSet:
import_location: university import_location: universityInfo
viewset_permission: IsStaff | NoPost viewset_permission: IsStaff | NoPost
api_end_point: universitiesInfo api_end_point: universitiesInfo
UniversitySemestersDatesViewSet: UniversitySemestersDatesViewSet:
import_location: university import_location: universitySemestersDates
api_end_point: universitiesSemestersDates api_end_point: universitiesSemestersDates
viewset_permission: IsStaff | NoPost viewset_permission: IsStaff | NoPost
UniversityDriViewSet: UniversityDriViewSet:
import_location: university import_location: universityDri
api_end_point: universityDri api_end_point: universityDri
api_attr: (?P<univ_id>[0-9]+) api_attr: (?P<univ_id>[0-9]+)
viewset_permission: IsStaff | IsDri | NoPost viewset_permission: IsStaff | IsDri | NoPost
...@@ -135,7 +135,7 @@ UniversityDriViewSet: ...@@ -135,7 +135,7 @@ UniversityDriViewSet:
CampusTaggedItemViewSet: CampusTaggedItemViewSet:
import_location: campus import_location: campusTaggedItem
api_end_point: campusTaggedItems api_end_point: campusTaggedItems
api_attr: (?P<campus_id>[0-9]+) api_attr: (?P<campus_id>[0-9]+)
...@@ -147,50 +147,50 @@ MainCampusViewSet: ...@@ -147,50 +147,50 @@ MainCampusViewSet:
RecommendationViewSet: RecommendationViewSet:
import_location: user import_location: recommendation
api_end_point: userRecommendations api_end_point: userRecommendations
RecommendationListViewSet: RecommendationListViewSet:
import_location: user import_location: recommendationList
api_end_point: userRecommendationLists api_end_point: userRecommendationLists
PreviousDepartureViewSet: PreviousDepartureViewSet:
import_location: user import_location: previousDeparture
api_end_point: universitiesPreviousDepartures api_end_point: universitiesPreviousDepartures
viewset_permission: ReadOnly viewset_permission: ReadOnly
PreviousDepartureFeedbackViewSet: PreviousDepartureFeedbackViewSet:
import_location: user import_location: previousDepartureFeedback
api_end_point: universitiesPreviousDepartureFeedback api_end_point: universitiesPreviousDepartureFeedback
PendingModerationViewSet: PendingModerationViewSet:
import_location: abstract.my_model import_location: pendingModeration
api_end_point: pendingModeration api_end_point: pendingModeration
viewset_permission: IsStaff viewset_permission: IsStaff
VersionViewSet: VersionViewSet:
import_location: abstract.my_model import_location: version
api_end_point: versions api_end_point: versions
api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+) api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+)
api_name: versionsList api_name: versionsList
viewset_permission: IsStaff | ReadOnly viewset_permission: IsStaff | ReadOnly
PendingModerationObjViewSet: PendingModerationObjViewSet:
import_location: abstract.my_model import_location: pendingModeration
api_end_point: pendingModerationObj api_end_point: pendingModerationObj
api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+) api_attr: (?P<content_type_id>[0-9]+)/(?P<object_pk>[0-9A-Za-z]+)
api_name: pendingModerationObj api_name: pendingModerationObj
viewset_permission: ReadOnly viewset_permission: ReadOnly
ForTestingModerationViewSet: ForTestingModerationViewSet:
import_location: abstract.my_model import_location: for_testing.moderation
api_end_point: test/moderation api_end_point: test/moderation
requires_testing: true requires_testing: true
ForTestingVersioningViewSet: ForTestingVersioningViewSet:
import_location: abstract.my_model import_location: for_testing.versioning
api_end_point: test/versioning api_end_point: test/versioning
requires_testing: true requires_testing: true
from .mySerializerWithJSON import MySerializerWithJSON
__all__ = ["MySerializerWithJSON"]
...@@ -2,7 +2,7 @@ from base_app.models import User ...@@ -2,7 +2,7 @@ from base_app.models import User
from django.utils import timezone from django.utils import timezone
import reversion import reversion
from backend_app.models.abstract.my_model import MyModel from backend_app.models.abstract.essentialModule import EssentialModule
class LoadGeneric(object): class LoadGeneric(object):
...@@ -10,7 +10,7 @@ class LoadGeneric(object): ...@@ -10,7 +10,7 @@ class LoadGeneric(object):
""" """
@classmethod @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(): with reversion.create_revision():
obj.moderated_by = admin obj.moderated_by = admin
obj.updated_by = admin obj.updated_by = admin
......
...@@ -2,16 +2,15 @@ from datetime import datetime ...@@ -2,16 +2,15 @@ from datetime import datetime
from base_app.models import User 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.currency import Currency
from backend_app.models.tag import Tag from backend_app.models.tag import Tag
from backend_app.models.university import ( from backend_app.models.university import University
University, from backend_app.models.universityDri import UniversityDri
UniversityDri, from backend_app.models.universityInfo import UniversityInfo
UniversityInfo, from backend_app.models.universitySemestersDates import UniversitySemestersDates
UniversitySemestersDates, from backend_app.models.universityTaggedItem import UniversityTaggedItem
UniversityTaggedItem,
)
from .loadGeneric import LoadGeneric 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")) 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.contrib.contenttypes.models import ContentType
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers
from backend_app.custom import MySerializerWithJSON from backend_app.permissions.moderation import is_moderation_required
from backend_app.permissions import is_moderation_required from backend_app.permissions.utils import Request as FakeRequest
from backend_app.utils import get_user_level from backend_app.utils import get_user_level
from rest_framework import serializers from base_app.models import User
from rest_framework.validators import ValidationError from django.contrib.contenttypes.fields import GenericRelation
from backend_app.config.other import DEFAULT_OBJ_MODERATION_LV 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.
from .myModel import MyModel All the logic behind moderation is done in EssentialModuleSerializer
from .pendingModeration import PendingModeration """
CLEANED_MY_MODEL_DATA = { # 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(