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

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):
"""
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
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.
http://localhost:5000/#/Application/Backend/models_serializers_viewsets
"""
# 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