2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-14 19:15:41 +00:00

[Feature] Override global settings (#9718)

* Provide mechanism for config to override global settings

* Ensure overrides are observed on save

* Refactor

* Add "read_only" field to serializer

* Prevent editing for read_only settings

* Bump API version

* Update docs

* Secure logs

* Override applies to default_value

* override get_setting method

* Add unit test

* Utilize new approach to override SITE_URL

* Docs updates

* Docs tweaks

* Shortcut for get_global_setting

* Remove previous change

- Allow validation to be performed within the InvenTreeSetting class

* Override INVENTREE_BASE_URL setting

* Handle error on worker boot

* Tweak unit test
This commit is contained in:
Oliver
2025-06-07 20:08:34 +10:00
committed by GitHub
parent 12677ccf22
commit 4b71130ebe
14 changed files with 178 additions and 23 deletions

View File

@ -1,12 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 345
INVENTREE_API_VERSION = 346
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v346 -> 2025-06-07 : https://github.com/inventree/InvenTree/pull/9718
- Adds "read_only" field to the GlobalSettings API endpoint(s)
v345 -> 2025-06-07 : https://github.com/inventree/InvenTree/pull/9745
- Adds barcode information to SalesOrderShipment API endpoint

View File

@ -17,7 +17,6 @@ from allauth.socialaccount.signals import social_account_updated
import InvenTree.conversion
import InvenTree.ready
import InvenTree.tasks
from common.settings import get_global_setting, set_global_setting
from InvenTree.config import get_setting
logger = structlog.get_logger('inventree')
@ -255,7 +254,6 @@ class InvenTreeConfig(AppConfig):
def update_site_url(self):
"""Update the site URL setting.
- If a fixed SITE_URL is specified (via configuration), it should override the INVENTREE_BASE_URL setting
- If multi-site support is enabled, update the site URL for the current site
"""
if not InvenTree.ready.canAppAccessDatabase():
@ -265,22 +263,16 @@ class InvenTreeConfig(AppConfig):
return
if settings.SITE_URL:
try:
if get_global_setting('INVENTREE_BASE_URL') != settings.SITE_URL:
set_global_setting('INVENTREE_BASE_URL', settings.SITE_URL)
logger.info('Updated INVENTREE_SITE_URL to %s', settings.SITE_URL)
except Exception:
pass
# If multi-site support is enabled, update the site URL for the current site
try:
from django.contrib.sites.models import Site
site = Site.objects.get_current()
site.domain = settings.SITE_URL
site.save()
logger.info('Updated current site URL to %s', settings.SITE_URL)
if site and site.domain != settings.SITE_URL:
site.domain = settings.SITE_URL
site.save()
logger.info('Updated current site URL to %s', settings.SITE_URL)
except Exception:
pass

View File

@ -1397,6 +1397,22 @@ TESTING_TABLE_EVENTS = False
# Flag to allow pricing recalculations during testing
TESTING_PRICING = False
# Global settings overrides
# If provided, these values will override any "global" settings (and prevent them from being set)
GLOBAL_SETTINGS_OVERRIDES = get_setting(
'INVENTREE_GLOBAL_SETTINGS', 'global_settings', typecast=dict
)
# Override site URL setting
if SITE_URL:
GLOBAL_SETTINGS_OVERRIDES['INVENTREE_BASE_URL'] = SITE_URL
if len(GLOBAL_SETTINGS_OVERRIDES) > 0:
logger.info('Global settings overrides: %s', str(GLOBAL_SETTINGS_OVERRIDES))
for key in GLOBAL_SETTINGS_OVERRIDES:
# Set the global setting
logger.debug('- Override value for %s = ********', key)
# User interface customization values
CUSTOM_LOGO = get_custom_file(
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True

View File

@ -584,12 +584,9 @@ class FormatTest(TestCase):
class TestHelpers(TestCase):
"""Tests for InvenTree helper functions."""
@override_settings(SITE_URL=None)
def test_absolute_url(self):
"""Test helper function for generating an absolute URL."""
base = 'https://demo.inventree.org:12345'
InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
base = InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
tests = {
'': base,

View File

@ -5,7 +5,11 @@ from django.apps import AppConfig
import structlog
import InvenTree.ready
from common.settings import get_global_setting, set_global_setting
from common.settings import (
get_global_setting,
global_setting_overrides,
set_global_setting,
)
logger = structlog.get_logger('inventree')
@ -24,6 +28,7 @@ class CommonConfig(AppConfig):
return
self.clear_restart_flag()
self.override_global_settings()
def clear_restart_flag(self):
"""Clear the SERVER_RESTART_REQUIRED setting."""
@ -37,3 +42,24 @@ class CommonConfig(AppConfig):
set_global_setting('SERVER_RESTART_REQUIRED', False, None)
except Exception:
pass
def override_global_settings(self):
"""Update global settings based on environment variables."""
overrides = global_setting_overrides()
if not overrides:
return
for key, value in overrides.items():
try:
current_value = get_global_setting(key, create=False)
if current_value != value:
logger.info(
'Overriding global setting: %s = %s', value, current_value
)
set_global_setting(key, value, None, create=True)
except Exception:
logger.warning('Failed to override global setting %s -> %s', key, value)
continue

View File

@ -46,6 +46,7 @@ import InvenTree.models
import InvenTree.ready
import users.models
from common.setting.type import InvenTreeSettingsKeyType, SettingsKeyType
from common.settings import global_setting_overrides
from generic.enums import StringEnum
from generic.states import ColorEnum
from generic.states.custom import state_color_mappings
@ -1157,11 +1158,42 @@ class InvenTreeSetting(BaseInvenTreeSetting):
If so, set the "SERVER_RESTART_REQUIRED" setting to True
"""
overrides = global_setting_overrides()
# If an override is specified for this setting, use that value
if self.key in overrides:
self.value = overrides[self.key]
super().save()
if self.requires_restart() and not InvenTree.ready.isImportingData():
InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', True, None)
@classmethod
def get_setting_default(cls, key, **kwargs):
"""Return the default value a particular setting."""
overrides = global_setting_overrides()
if key in overrides:
# If an override is specified for this setting, use that value
return overrides[key]
return super().get_setting_default(key, **kwargs)
@classmethod
def get_setting(cls, key, backup_value=None, **kwargs):
"""Get the value of a particular setting.
If it does not exist, return the backup value (default = None)
"""
overrides = global_setting_overrides()
if key in overrides:
# If an override is specified for this setting, use that value
return overrides[key]
return super().get_setting(key, backup_value=backup_value, **kwargs)
"""
Dict of all global settings values:

View File

@ -15,6 +15,7 @@ from rest_framework.exceptions import PermissionDenied
from taggit.serializers import TagListSerializerField
import common.models as common_models
import common.settings
import common.validators
import generic.states.custom
from importer.registry import register_importer
@ -123,8 +124,28 @@ class GlobalSettingsSerializer(SettingsSerializer):
'model_name',
'api_url',
'typ',
'read_only',
]
read_only = serializers.SerializerMethodField(
read_only=True,
help_text=_(
'Indicates if the setting is overridden by an environment variable'
),
label=_('Override'),
)
def get_read_only(self, obj) -> bool:
"""Return True if the setting 'read_only' (cannot be edited).
A setting may be "read-only" if:
- It is overridden by an environment variable.
"""
overrides = common.settings.global_setting_overrides()
return obj.key in overrides
class UserSettingsSerializer(SettingsSerializer):
"""Serializer for the InvenTreeUserSetting model."""

View File

@ -3,6 +3,19 @@
from os import environ
def global_setting_overrides() -> dict:
"""Return a dictionary of global settings overrides.
These values are set via environment variables or configuration file.
"""
from django.conf import settings
if hasattr(settings, 'GLOBAL_SETTINGS_OVERRIDES'):
return settings.GLOBAL_SETTINGS_OVERRIDES or {}
return {}
def get_global_setting(key, backup_value=None, enviroment_key=None, **kwargs):
"""Return the value of a global setting using the provided key."""
from common.models import InvenTreeSetting

View File

@ -451,6 +451,15 @@ class SettingsTest(InvenTreeTestCase):
f'Non-boolean default value specified for {key}'
) # pragma: no cover
@override_settings(
GLOBAL_SETTINGS_OVERRIDES={'INVENTREE_INSTANCE': 'Overridden Instance Name'}
)
def test_override(self):
"""Test override of global settings."""
self.assertEqual(
get_global_setting('INVENTREE_INSTANCE'), 'Overridden Instance Name'
)
def test_global_setting_caching(self):
"""Test caching operations for the global settings class."""
key = 'PART_NAME_FORMAT'

View File

@ -211,3 +211,8 @@ ldap:
# hide_password_reset: true
# logo: img/custom_logo.png
# splash: img/custom_splash.jpg
# Override global settings
#global_settings:
# INVENTREE_DEFAULT_CURRENCY: 'CNY'
# INVENTREE_RESTRICT_ABOUT: true

View File

@ -33,6 +33,7 @@ export interface Setting {
plugin?: string;
method?: string;
required?: boolean;
read_only?: boolean;
}
export interface SettingChoice {

View File

@ -9,7 +9,7 @@ import {
useMantineColorScheme
} from '@mantine/core';
import { IconEdit } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import { ModelType } from '@lib/enums/ModelType';
@ -51,6 +51,23 @@ function SettingValue({
const [modelInstance, setModelInstance] = useState<any>(null);
// Launch the edit dialog for this setting
const editSetting = useCallback(() => {
if (!setting.read_only) {
onEdit(setting);
}
}, [setting, onEdit]);
// Toggle the setting value (if it is a boolean)
const toggleSetting = useCallback(
(event: any) => {
if (!setting.read_only) {
onToggle(setting, event.currentTarget.checked);
}
},
[setting, onToggle]
);
// Does this setting map to an internal database model?
const modelType: ModelType | null = useMemo(() => {
if (setting.model_name) {
@ -89,7 +106,8 @@ function SettingValue({
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
disabled={setting.read_only}
onClick={editSetting}
>
<IconEdit />
</Button>
@ -104,8 +122,9 @@ function SettingValue({
size='sm'
radius='lg'
aria-label={`toggle-setting-${setting.key}`}
disabled={setting.read_only}
checked={setting.value.toLowerCase() == 'true'}
onChange={(event) => onToggle(setting, event.currentTarget.checked)}
onChange={toggleSetting}
style={{
paddingRight: '20px'
}}
@ -118,7 +137,8 @@ function SettingValue({
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
disabled={setting.read_only}
onClick={editSetting}
>
{valueText}
</Button>
@ -127,7 +147,8 @@ function SettingValue({
<Button
aria-label={`edit-setting-${setting.key}`}
variant='subtle'
onClick={() => onEdit(setting)}
disabled={setting.read_only}
onClick={editSetting}
>
<IconEdit />
</Button>