2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 19:46:46 +00:00

[FR] Simplify / optimize setting loading (#4152)

* [FR] Simplify / optimize setting loading
Cache config.yaml data on load and use cached  for get_settings
Fixes #4149

* move the cache setting to config

* add docstring

* spell fix

* Add lookup where settings come from
Fixes #3982

* Fix spelling
This commit is contained in:
Matthias Mair 2023-01-07 13:24:20 +01:00 committed by GitHub
parent 0e96654b6a
commit 82bdd7780d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 10 deletions

View File

@ -1,5 +1,6 @@
"""Helper functions for loading InvenTree configuration options.""" """Helper functions for loading InvenTree configuration options."""
import datetime
import logging import logging
import os import os
import random import random
@ -8,6 +9,8 @@ import string
from pathlib import Path from pathlib import Path
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
CONFIG_DATA = None
CONFIG_LOOKUPS = {}
def is_true(x): def is_true(x):
@ -56,8 +59,18 @@ def get_config_file(create=True) -> Path:
return cfg_filename return cfg_filename
def load_config_data() -> map: def load_config_data(set_cache: bool = False) -> map:
"""Load configuration data from the config file.""" """Load configuration data from the config file.
Arguments:
set_cache(bool): If True, the configuration data will be cached for future use after load.
"""
global CONFIG_DATA
# use cache if populated
# skip cache if cache should be set
if CONFIG_DATA is not None and not set_cache:
return CONFIG_DATA
import yaml import yaml
@ -66,6 +79,10 @@ def load_config_data() -> map:
with open(cfg_file, 'r') as cfg: with open(cfg_file, 'r') as cfg:
data = yaml.safe_load(cfg) data = yaml.safe_load(cfg)
# Set the cache if requested
if set_cache:
CONFIG_DATA = data
return data return data
@ -82,22 +99,30 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
default_value: Value to return if first two options are not provided default_value: Value to return if first two options are not provided
typecast: Function to use for typecasting the value typecast: Function to use for typecasting the value
""" """
def try_typecasting(value): def try_typecasting(value, source: str):
"""Attempt to typecast the value""" """Attempt to typecast the value"""
if typecast is not None: if typecast is not None:
# Try to typecast the value # Try to typecast the value
try: try:
return typecast(value) val = typecast(value)
set_metadata(source)
return val
except Exception as error: except Exception as error:
logger.error(f"Failed to typecast '{env_var}' with value '{value}' to type '{typecast}' with error {error}") logger.error(f"Failed to typecast '{env_var}' with value '{value}' to type '{typecast}' with error {error}")
set_metadata(source)
return value return value
def set_metadata(source: str):
"""Set lookup metadata for the setting."""
key = env_var or config_key
CONFIG_LOOKUPS[key] = {'env_var': env_var, 'config_key': config_key, 'source': source, 'accessed': datetime.datetime.now()}
# First, try to load from the environment variables # First, try to load from the environment variables
if env_var is not None: if env_var is not None:
val = os.getenv(env_var, None) val = os.getenv(env_var, None)
if val is not None: if val is not None:
return try_typecasting(val) return try_typecasting(val, 'env')
# Next, try to load from configuration file # Next, try to load from configuration file
if config_key is not None: if config_key is not None:
@ -116,10 +141,10 @@ def get_setting(env_var=None, config_key=None, default_value=None, typecast=None
cfg_data = cfg_data[key] cfg_data = cfg_data[key]
if result is not None: if result is not None:
return try_typecasting(result) return try_typecasting(result, 'yaml')
# Finally, return the default value # Finally, return the default value
return try_typecasting(default_value) return try_typecasting(default_value, 'default')
def get_boolean_setting(env_var=None, config_key=None, default_value=False): def get_boolean_setting(env_var=None, config_key=None, default_value=False):

View File

@ -67,6 +67,14 @@ class RolePermission(permissions.BasePermission):
return result return result
class IsSuperuser(permissions.IsAdminUser):
"""Allows access only to superuser users."""
def has_permission(self, request, view):
"""Check if the user is a superuser."""
return bool(request.user and request.user.is_superuser)
def auth_exempt(view_func): def auth_exempt(view_func):
"""Mark a view function as being exempt from auth requirements.""" """Mark a view function as being exempt from auth requirements."""
def wrapped_view(*args, **kwargs): def wrapped_view(*args, **kwargs):

View File

@ -52,7 +52,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
BASE_DIR = config.get_base_dir() BASE_DIR = config.get_base_dir()
# Load configuration data # Load configuration data
CONFIG = config.load_config_data() CONFIG = config.load_config_data(set_cache=True)
# Default action is to run the system in Debug mode # Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!

View File

@ -13,7 +13,7 @@ from rest_framework.documentation import include_docs_urls
from build.api import build_api_urls from build.api import build_api_urls
from build.urls import build_urls from build.urls import build_urls
from common.api import common_api_urls, settings_api_urls from common.api import admin_api_urls, common_api_urls, settings_api_urls
from common.urls import common_urls from common.urls import common_urls
from company.api import company_api_urls from company.api import company_api_urls
from company.urls import (company_urls, manufacturer_part_urls, from company.urls import (company_urls, manufacturer_part_urls,
@ -53,6 +53,7 @@ apipatterns = [
re_path(r'^label/', include(label_api_urls)), re_path(r'^label/', include(label_api_urls)),
re_path(r'^report/', include(report_api_urls)), re_path(r'^report/', include(report_api_urls)),
re_path(r'^user/', include(user_urls)), re_path(r'^user/', include(user_urls)),
re_path(r'^admin/', include(admin_api_urls)),
# Plugin endpoints # Plugin endpoints
path('', include(plugin_api_urls)), path('', include(plugin_api_urls)),

View File

@ -18,9 +18,11 @@ from rest_framework.views import APIView
import common.models import common.models
import common.serializers import common.serializers
from InvenTree.api import BulkDeleteMixin from InvenTree.api import BulkDeleteMixin
from InvenTree.config import CONFIG_LOOKUPS
from InvenTree.helpers import inheritors from InvenTree.helpers import inheritors
from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI, from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI,
RetrieveUpdateDestroyAPI) RetrieveUpdateDestroyAPI)
from InvenTree.permissions import IsSuperuser
from plugin.models import NotificationUserSetting from plugin.models import NotificationUserSetting
from plugin.serializers import NotificationUserSettingSerializer from plugin.serializers import NotificationUserSettingSerializer
@ -360,6 +362,29 @@ class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
"""Detail view for an individual news feed object.""" """Detail view for an individual news feed object."""
class ConfigList(ListAPI):
"""List view for all accessed configurations."""
queryset = CONFIG_LOOKUPS
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser, ]
class ConfigDetail(RetrieveAPI):
"""Detail view for an individual configuration."""
serializer_class = common.serializers.ConfigSerializer
permission_classes = [IsSuperuser, ]
def get_object(self):
"""Attempt to find a config object with the provided key."""
key = self.kwargs['key']
value = CONFIG_LOOKUPS.get(key, None)
if not value:
raise NotFound()
return {key: value}
settings_api_urls = [ settings_api_urls = [
# User settings # User settings
re_path(r'^user/', include([ re_path(r'^user/', include([
@ -415,3 +440,9 @@ common_api_urls = [
])), ])),
] ]
admin_api_urls = [
# Admin
path('config/', ConfigList.as_view(), name='api-config-list'),
path('config/<str:key>/', ConfigDetail.as_view(), name='api-config-detail'),
]

View File

@ -222,3 +222,16 @@ class NewsFeedEntrySerializer(InvenTreeModelSerializer):
'summary', 'summary',
'read', 'read',
] ]
class ConfigSerializer(serializers.Serializer):
"""Serializer for the InvenTree configuration.
This is a read-only serializer.
"""
def to_representation(self, instance):
"""Return the configuration data as a dictionary."""
if not isinstance(instance, str):
instance = list(instance.keys())[0]
return {'key': instance, **self.instance[instance]}

View File

@ -827,7 +827,7 @@ class NotificationTest(InvenTreeAPITestCase):
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3) self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
class LoadingTest(TestCase): class CommonTest(InvenTreeAPITestCase):
"""Tests for the common config.""" """Tests for the common config."""
def test_restart_flag(self): def test_restart_flag(self):
@ -844,6 +844,30 @@ class LoadingTest(TestCase):
# now it should be false again # now it should be false again
self.assertFalse(common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED')) self.assertFalse(common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'))
def test_config_api(self):
"""Test config URLs."""
# Not superuser
self.get(reverse('api-config-list'), expected_code=403)
# Turn into superuser
self.user.is_superuser = True
self.user.save()
# Successfull checks
data = [
self.get(reverse('api-config-list'), expected_code=200).data[0], # list endpoint
self.get(reverse('api-config-detail', kwargs={'key': 'INVENTREE_DEBUG'}), expected_code=200).data, # detail endpoint
]
for item in data:
self.assertEqual(item['key'], 'INVENTREE_DEBUG')
self.assertEqual(item['env_var'], 'INVENTREE_DEBUG')
self.assertEqual(item['config_key'], 'debug')
# Turn into normal user again
self.user.is_superuser = False
self.user.save()
class ColorThemeTest(TestCase): class ColorThemeTest(TestCase):
"""Tests for ColorTheme.""" """Tests for ColorTheme."""