Commit 243f43bf authored by Florent Chehab's avatar Florent Chehab

refactor(backend): removed dynamic imports 馃帄

* Removed all dynamic imports to have more standard Django infrastructre and ease future refactoring
* Removed now useless config files
* Returns the list of available endpoints to the frontend directly from the html
* updated documentation accordingly

Fixes #95
parent 77cce1b1
Pipeline #37638 passed with stages
in 5 minutes and 16 seconds
from django.contrib import admin
from reversion_compare.admin import CompareVersionAdmin
from backend_app.config.models import get_models
# 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(versioned=True) # , requires_testing=False)
CLASSIC_MODELS = get_models(versioned=False) # , requires_testing=False)
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
from backend_app.models.campus import Campus
from backend_app.models.campusTaggedItem import CampusTaggedItem
from backend_app.models.city import City
from backend_app.models.cityTaggedItem import CityTaggedItem
from backend_app.models.country import Country
from backend_app.models.countryDri import CountryDri
from backend_app.models.countryScholarship import CountryScholarship
from backend_app.models.countryTaggedItem import CountryTaggedItem
from backend_app.models.currency import Currency
from backend_app.models.department import Department
from backend_app.models.for_testing.moderation import ForTestingModeration
from backend_app.models.for_testing.versioning import ForTestingVersioning
from backend_app.models.offer import Offer
from backend_app.models.pendingModeration import PendingModeration
from backend_app.models.previousDeparture import PreviousDeparture
from backend_app.models.previousDepartureFeedback import PreviousDepartureFeedback
from backend_app.models.recommendation import Recommendation
from backend_app.models.recommendationList import RecommendationList
from backend_app.models.specialty import Specialty
from backend_app.models.tag import Tag
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.universityScholarship import UniversityScholarship
from backend_app.models.universitySemestersDates import UniversitySemestersDates
from backend_app.models.universityTaggedItem import UniversityTaggedItem
from backend_app.models.userData import UserData
from backend_app.models.version import Version
ALL_MODELS = [
Campus,
CampusTaggedItem,
City,
CityTaggedItem,
Country,
CountryDri,
CountryScholarship,
CountryTaggedItem,
Currency,
Department,
Offer,
PendingModeration,
PreviousDeparture,
PreviousDepartureFeedback,
Recommendation,
RecommendationList,
Specialty,
Tag,
University,
UniversityDri,
UniversityInfo,
UniversityScholarship,
UniversitySemestersDates,
UniversityTaggedItem,
UserData,
Version,
]
# We also register testing to models to make sure migrations are created for them
ALL_MODELS += [ForTestingModeration, ForTestingVersioning]
CLASSIC_MODELS = filter(
lambda m: not issubclass(m, VersionedEssentialModule), ALL_MODELS
)
VERSIONED_MODELS = filter(lambda m: issubclass(m, VersionedEssentialModule), ALL_MODELS)
#######
# Register the models
......
from collections import Counter
from backend_app.models.abstract.base import BaseModelViewSet
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
from backend_app.permissions.utils import Request, FakeUser
def check_viewsets(viewsets):
def check_viewsets(all_viewsets):
"""
Check that if 2 serializers are registered for the same model. Then that model
has a get_serializer method to point to the serializer to use to deserialize it.
There should be only one of serializer being used per model. Otherwise extra
configuration is required.
This function performs multiple checks on the viewsets to make sure they are well configured.
First for all types of viewsets.
Check 1: all viewsets have an end_point_route attribute
Check 2: all viewsets have working permissions classes
See: https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Backend/models_serializers_viewsets?id=viewsets
http://localhost:5000/#/Application/Backend/models_serializers_viewsets
Then for REST enpoints:
Check 3:
Check that if 2 serializers are registered for the same model. Then that model
has a get_serializer method to point to the serializer to use to deserialize it.
There should be only one of serializer being used per model. Otherwise extra
configuration is required.
"""
# Prevent cyclic imports
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
# Check 1
for v in all_viewsets:
if v.end_point_route is None:
raise Exception(
"You forget to configure the `end_point_route` "
"attribute in the viewset {}".format(v)
)
# Check 2
fake_request = Request(FakeUser(["DRI"]), "PUT")
for v in all_viewsets:
try:
for p in v().get_permissions():
p.has_permission(fake_request, None)
except TypeError:
raise Exception(
"`permission_classes` on viewset {} are misconfigured.\n"
"Have a look at the documentation: "
"https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Backend/models_serializers_viewsets?id=viewsets".format(
v
)
)
# Check 3
serializers = list()
models = []
for viewset in viewsets:
serializer = viewset().get_serializer_class()
for v in filter(lambda v: issubclass(v, BaseModelViewSet), all_viewsets):
serializer = v().get_serializer_class()
model = serializer.Meta.model
if issubclass(model, VersionedEssentialModule):
......
import importlib
from typing import List, Optional, Union
from backend_app.models.abstract.versionedEssentialModule import (
VersionedEssentialModule,
)
from django.conf import settings
from .utils import load_viewsets_config
def get_models(
versioned: Optional[bool] = None,
requires_testing: Union[None, bool, "smart"] = None,
) -> List[object]:
"""
Returns the list of models that could be ingered from the `viewsets_config.yml`
config file.
Some filtering can be applied to some attributes.
If the parameter is `None` then no filtering is applied to this attribute.
If the parameter is `True` or `False` the object is returned only if it matched.
There is one exception for the parameter `requires_testing` if it is set to `smart` then
the object is returned only if doesn't require testing or if testing is activated.
"""
out = list()
for obj in load_viewsets_config():
if requires_testing is not None:
if requires_testing == "smart":
if obj.requires_testing and not settings.TESTING:
continue
else:
if requires_testing and not settings.TESTING:
continue
if requires_testing and not obj.requires_testing:
continue
if not requires_testing and obj.requires_testing:
continue
module = importlib.import_module(
"backend_app.models.{}".format(obj.import_location)
)
Viewset = getattr(module, obj.viewset)
if not hasattr(Viewset, "serializer_class"):
# It is an API viewset, we don't care about it
# API viewsets don't correspond to a model.
continue
Model = Viewset.serializer_class.Meta.model
if versioned is not None:
if versioned and not issubclass(Model, VersionedEssentialModule):
continue
if not versioned and issubclass(Model, VersionedEssentialModule):
continue
out.append(Model)
return list(set(out)) # make sure we return each models at most once
# THIS FILE IS DYNAMICALLY USED FOR THE BACKEND
# TAKE CARE WHEN MODYFING IT ;)
#################################################
# It contains information regarding ow models
# are configured.
# A restart of the backend server is required
# for it to take effect.
#################################################
# Look at the documentation about config files to know more about this
# http://localhost:5000/#/Application/Backend/config_files?id=models_configyml
# https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Backend/config_files?id=models_configyml
UserData:
moderation_level: 0
Recommendation:
moderation_level: 0
RecommendationList:
moderation_level: 0
ForTestingModeration:
moderation_level: 1
ForTestingVersioning:
moderation_level: 1
from os.path import dirname, join, realpath
from typing import List
import yaml
from dotmap import DotMap
CURRENT_DIR = dirname(realpath(__file__))
def get_yml_file(name):
"""
Helper function to load config files.
"""
with open(join(CURRENT_DIR, name), "r") as f:
return yaml.load(f)
def load_viewsets_config() -> List[DotMap]:
"""
Returns the list of api objects without filtering add default attributes
added to all objects if they are missing.
"""
tmp = get_yml_file("viewsets_config.yml")
api_config = []
for viewset in tmp.keys():
obj = tmp[viewset]
obj["viewset"] = viewset
api_config.append(obj)
# clean api_config (add default arguments)
DEFAULT_SETTINGS = {
"is_api_view": False,
"requires_testing": False,
"viewset_permission": "default",
"api_attr": None,
"api_name": None,
}
for obj in api_config:
for key in DEFAULT_SETTINGS:
if key not in obj:
obj[key] = DEFAULT_SETTINGS[key]
return [DotMap(obj) for obj in api_config]
def load_models_config() -> DotMap:
"""
Load the `models_config.yml` config file and returns it as
a `DotMap`/dict. No transformation whatsoever is applied.
"""
data = get_yml_file("models_config.yml")
return DotMap(data)
def load_defaults() -> DotMap:
"""
Load the `defaults.yml` config file and returns it as
a `DotMap`/dict. No transformation whatsoever is applied.
"""
data = get_yml_file("defaults.yml")
return DotMap(data)
import importlib
from typing import List, Optional, Union
from django.conf import settings
from dotmap import DotMap
from .utils import load_viewsets_config
def get_viewsets_info(
ignore_in_admin: Optional[bool] = None,
requires_testing: Union[None, bool, "smart"] = None,
is_api_view: Optional[bool] = False,
) -> List[DotMap]:
"""
Returns a list of DotMap objects corresponding the api config file
with filtering applied to some attributes.
If the parameter is `None` then no filtering is applied to this attribute.
If the parameter is `True` or `False` the object is returned only if it matches.
There is one exception for the parameter `requires_testing` if it is set to `smart` then
the object is returned only if doesn't require testing or if testing is activated.
"""
out = list()
for obj in load_viewsets_config():
if ignore_in_admin is not None:
if ignore_in_admin and not obj.ignore_in_admin:
continue
if not ignore_in_admin and obj.ignore_in_admin:
continue
if requires_testing is not None:
if requires_testing == "smart":
if obj.requires_testing and not settings.TESTING:
continue
else:
if requires_testing and not settings.TESTING:
continue
if requires_testing and not obj.requires_testing:
continue
if not requires_testing and obj.requires_testing:
continue
if is_api_view is not None:
if is_api_view and not obj.is_api_view:
continue
if not is_api_view and obj.is_api_view:
continue
module = importlib.import_module(
"backend_app.models.{}".format(obj.import_location)
)
Viewset = getattr(module, obj.viewset)
obj.Viewset = Viewset
out.append(obj)
return out
# THIS FILE IS DYNAMICALLY USED FOR THE BACKEND AND THE FRONTEND
# TAKE CARE WHEN MODYFING IT ;)
#################################################
# It contains information regarding
# how viewsets are configured.
# A restart of the backend server and a full
# recompile of the frontend is required
# for changes it to take effect.
#################################################
# Look at the documentation about config files to know more about this
# http://localhost:5000/#/Application/Backend/config_files?id=viewsets_configyml
# https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Backend/config_files?id=viewsets_configyml
#####################################################
## Custom Viewsets that doesn't have a model behind
#####################################################
AppModerationStatusViewSet:
api_end_point: serverModerationStatus
import_location: other_viewsets
viewset_permission: ReadOnly
is_api_view: true
#####################
## Standard Viewsets
#####################
CountryViewSet:
import_location: country
api_end_point: countries
viewset_permission: IsStaff | ReadOnly
CityViewSet:
import_location: city
api_end_point: cities
viewset_permission: IsStaff | NoPost
UniversityViewSet:
import_location: university
api_end_point: universities
viewset_permission: IsStaff | NoPost
CampusViewSet:
import_location: campus
api_end_point: campuses
UserDataViewSet:
import_location: userData
api_end_point: userData
api_name: user-data-detail
viewset_permission: IsOwner
TagViewSet:
import_location: tag
api_end_point: tags
viewset_permission: IsStaff | ReadOnly
CurrencyViewSet:
import_location: currency
api_end_point: currencies
viewset_permission: IsStaff | ReadOnly
DepartmentViewSet:
import_location: department
api_end_point: departments
viewset_permission: IsStaff | ReadOnly
SpecialtyViewSet:
import_location: specialty
api_end_point: specialties
viewset_permission: IsStaff | ReadOnly
OfferViewSet:
import_location: offer
api_end_point: offers
viewset_permission: IsStaff | ReadOnly
CountryTaggedItemViewSet:
import_location: countryTaggedItem
api_end_point: countryTaggedItems
api_attr: (?P<country_id>[a-zA-Z]+)
CountryScholarshipViewSet:
import_location: countryScholarship
api_end_point: countryScholarships
api_attr: (?P<country_id>[a-zA-Z]+)
CountryDriViewSet:
import_location: countryDri
api_end_point: countryDri
api_attr: (?P<country_id>[a-zA-Z]+)
viewset_permission: IsStaff | IsDri | NoPost
CityTaggedItemViewSet:
import_location: cityTaggedItem
api_end_point: cityTaggedItems
api_attr: (?P<city_id>[0-9]+)
UniversityTaggedItemViewSet:
import_location: universityTaggedItem
api_end_point: universityTaggedItems
api_attr: (?P<univ_id>[0-9]+)
UniversityScholarshipViewSet:
import_location: universityScholarship
api_end_point: universityScholarships
api_attr: (?P<univ_id>[0-9]+)
UniversityInfoViewSet:
import_location: universityInfo
viewset_permission: IsStaff | NoPost
api_end_point: universitiesInfo
UniversitySemestersDatesViewSet:
import_location: universitySemestersDates
api_end_point: universitiesSemestersDates
viewset_permission: IsStaff | NoPost
UniversityDriViewSet:
import_location: universityDri
api_end_point: universityDri
api_attr: (?P<univ_id>[0-9]+)
viewset_permission: IsStaff | IsDri | NoPost
CampusTaggedItemViewSet:
import_location: campusTaggedItem
api_end_point: campusTaggedItems
api_attr: (?P<campus_id>[0-9]+)
MainCampusViewSet:
import_location: campus
api_end_point: mainCampuses
viewset_permission: ReadOnly
RecommendationViewSet:
import_location: recommendation
api_end_point: userRecommendations
RecommendationListViewSet:
import_location: recommendationList
api_end_point: userRecommendationLists
PreviousDepartureViewSet:
import_location: previousDeparture
api_end_point: universitiesPreviousDepartures
viewset_permission: ReadOnly
PreviousDepartureFeedbackViewSet:
import_location: previousDepartureFeedback
api_end_point: universitiesPreviousDepartureFeedback
PendingModerationViewSet:
import_location: pendingModeration
api_end_point: pendingModeration
viewset_permission: IsStaff
VersionViewSet:
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: 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: for_testing.moderation
api_end_point: test/moderation
requires_testing: true
ForTestingVersioningViewSet:
import_location: for_testing.versioning
api_end_point: test/versioning
requires_testing: true
from django.conf import settings
from django.db import models
from rest_framework import serializers, viewsets
from backend_app.custom.mySerializerWithJSON import MySerializerWithJSON
from backend_app.permissions.viewsets import get_viewset_permissions
from rest_framework import serializers, viewsets
from backend_app.permissions.default import DEFAULT_VIEWSET_PERMISSIONS
class BaseModel(models.Model):
......@@ -15,6 +16,11 @@ class BaseModel(models.Model):
viewset.
"""
# Look at the documentation about config files to know more about this
# https://rex-dri.gitlab.utc.fr/rex-dri/documentation/#/Application/Backend/moderation_and_versioning?id=model-level
# http://localhost:5000/#/Application/Backend/moderation_and_versioning?id=model-level
moderation_level = settings.DEFAULT_MODEL_MODERATION_LEVEL
class Meta:
abstract = True
......@@ -60,17 +66,20 @@ class BaseModelViewSet(viewsets.ModelViewSet):
"""
serializer_class = BaseModelSerializer
permission_classes = None
_permission_classes_cached = None
# We store the api endpoint route directly in the viewset classes
# so that we can easily access them
end_point_route = None
@property
def permission_classes(self):
def get_permissions(self):
"""
We retrieve permissions classes for the viewsets
From the config file and cache it.
We override the permission getter to make sure we add the default
app viewsets permissions
"""
if self._permission_classes_cached is None:
self._permission_classes_cached = get_viewset_permissions(
self.__class__.__name__
)
return self._permission_classes_cached
if self.permission_classes is None:
return [DEFAULT_VIEWSET_PERMISSIONS()]
else:
return [
(DEFAULT_VIEWSET_PERMISSIONS & p)() for p in self.permission_classes
]
......@@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from backend_app.config.other import (
from backend_app.settings.defaults import (
DEFAULT_OBJ_MODERATION_LV,
OBJ_MODERATION_PERMISSIONS,
)
......
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from backend_app.models.abstract.module import Module, ModuleSerializer, ModuleViewSet
from backend_app.models.city import City
from backend_app.models.university import University
from django.core.validators import MinValueValidator, MaxValueValidator
from backend_app.permissions.app_permissions import ReadOnly
class Campus(Module):
is_main_campus = models.BooleanField(null=False)
name = models.CharField(max_length=200, default="", blank=True)
city = models.ForeignKey(City, on_delete=models.PROTECT, null=False)
......@@ -45,8 +46,11 @@ class CampusSerializer(ModuleSerializer):
class CampusViewSet(ModuleViewSet):
queryset = Campus.objects.all() # pylint: disable=E1101
serializer_class = CampusSerializer
end_point_route = "campuses"
class MainCampusViewSet(ModuleViewSet):
queryset = Campus.objects.filter(is_main_campus=True)
serializer_class = CampusSerializer
permission_classes = (ReadOnly,)
end_point_route = "mainCampuses"
from django.db import models
from backend_app.models.campus import Campus
from backend_app.models.abstract.taggedItem import (
TaggedItem,
TaggedItemSerializer,
TaggedItemViewSet,
)
from backend_app.models.campus import Campus
class CampusTaggedItem(TaggedItem):
campus = models.ForeignKey(
Campus, on_delete=models.PROTECT, related_name="campus_tagged_items"
)
......@@ -26,6 +26,7 @@ class CampusTaggedItemSerializer(TaggedItemSerializer):
class CampusTaggedItemViewSet(TaggedItemViewSet):
queryset = CampusTaggedItem.objects.all() # pylint: disable=E1101
serializer_class = CampusTaggedItemSerializer
end_point_route = r"campusTaggedItems/(?P<campus_id>[0-9]+)"
def get_queryset(self):
campus_id = self.kwargs["campus_id"]
......
......@@ -6,10 +6,10 @@ from backend_app.models.abstract.base import (
BaseModelViewSet,
)
from backend_app.models.country import Country
from backend_app.permissions.app_permissions import ReadOnly, IsStaff
class City(BaseModel):
name = models.CharField(max_length=200)
local_name = models.CharField(max_length=200, default="", blank=True)
# We add an area to distinguish similarly named cities
......@@ -27,3 +27,5 @@ class CitySerializer(BaseModelSerializer):
class CityViewSet(BaseModelViewSet):