mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
0e96654b6a
commit
82bdd7780d
@ -1,5 +1,6 @@
|
||||
"""Helper functions for loading InvenTree configuration options."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
@ -8,6 +9,8 @@ import string
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
CONFIG_DATA = None
|
||||
CONFIG_LOOKUPS = {}
|
||||
|
||||
|
||||
def is_true(x):
|
||||
@ -56,8 +59,18 @@ def get_config_file(create=True) -> Path:
|
||||
return cfg_filename
|
||||
|
||||
|
||||
def load_config_data() -> map:
|
||||
"""Load configuration data from the config file."""
|
||||
def load_config_data(set_cache: bool = False) -> map:
|
||||
"""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
|
||||
|
||||
@ -66,6 +79,10 @@ def load_config_data() -> map:
|
||||
with open(cfg_file, 'r') as cfg:
|
||||
data = yaml.safe_load(cfg)
|
||||
|
||||
# Set the cache if requested
|
||||
if set_cache:
|
||||
CONFIG_DATA = 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
|
||||
typecast: Function to use for typecasting the value
|
||||
"""
|
||||
def try_typecasting(value):
|
||||
def try_typecasting(value, source: str):
|
||||
"""Attempt to typecast the value"""
|
||||
if typecast is not None:
|
||||
# Try to typecast the value
|
||||
try:
|
||||
return typecast(value)
|
||||
val = typecast(value)
|
||||
set_metadata(source)
|
||||
return val
|
||||
except Exception as error:
|
||||
logger.error(f"Failed to typecast '{env_var}' with value '{value}' to type '{typecast}' with error {error}")
|
||||
set_metadata(source)
|
||||
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
|
||||
if env_var is not None:
|
||||
val = os.getenv(env_var, None)
|
||||
|
||||
if val is not None:
|
||||
return try_typecasting(val)
|
||||
return try_typecasting(val, 'env')
|
||||
|
||||
# Next, try to load from configuration file
|
||||
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]
|
||||
|
||||
if result is not None:
|
||||
return try_typecasting(result)
|
||||
return try_typecasting(result, 'yaml')
|
||||
|
||||
# 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):
|
||||
|
@ -67,6 +67,14 @@ class RolePermission(permissions.BasePermission):
|
||||
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):
|
||||
"""Mark a view function as being exempt from auth requirements."""
|
||||
def wrapped_view(*args, **kwargs):
|
||||
|
@ -52,7 +52,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
BASE_DIR = config.get_base_dir()
|
||||
|
||||
# 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
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
|
@ -13,7 +13,7 @@ from rest_framework.documentation import include_docs_urls
|
||||
|
||||
from build.api import build_api_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 company.api import company_api_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'^report/', include(report_api_urls)),
|
||||
re_path(r'^user/', include(user_urls)),
|
||||
re_path(r'^admin/', include(admin_api_urls)),
|
||||
|
||||
# Plugin endpoints
|
||||
path('', include(plugin_api_urls)),
|
||||
|
@ -18,9 +18,11 @@ from rest_framework.views import APIView
|
||||
import common.models
|
||||
import common.serializers
|
||||
from InvenTree.api import BulkDeleteMixin
|
||||
from InvenTree.config import CONFIG_LOOKUPS
|
||||
from InvenTree.helpers import inheritors
|
||||
from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI,
|
||||
RetrieveUpdateDestroyAPI)
|
||||
from InvenTree.permissions import IsSuperuser
|
||||
from plugin.models import NotificationUserSetting
|
||||
from plugin.serializers import NotificationUserSettingSerializer
|
||||
|
||||
@ -360,6 +362,29 @@ class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
|
||||
"""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 = [
|
||||
# User settings
|
||||
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'),
|
||||
]
|
||||
|
@ -222,3 +222,16 @@ class NewsFeedEntrySerializer(InvenTreeModelSerializer):
|
||||
'summary',
|
||||
'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]}
|
||||
|
@ -827,7 +827,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3)
|
||||
|
||||
|
||||
class LoadingTest(TestCase):
|
||||
class CommonTest(InvenTreeAPITestCase):
|
||||
"""Tests for the common config."""
|
||||
|
||||
def test_restart_flag(self):
|
||||
@ -844,6 +844,30 @@ class LoadingTest(TestCase):
|
||||
# now it should be false again
|
||||
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):
|
||||
"""Tests for ColorTheme."""
|
||||
|
Loading…
Reference in New Issue
Block a user