Coverage for src/backend/InvenTree/InvenTree/settings.py: 67%
384 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Django settings for InvenTree project.
3In practice the settings in this file should not be adjusted,
4instead settings can be configured in the config.yaml file
5located in the top level project directory.
7This allows implementation configuration to be hidden from source control,
8as well as separate configuration parameters from the more complex
9database setup in this file.
10"""
12import logging
13import os
14import sys
15from typing import Optional
16from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
18import django.conf.locale
19import django.core.exceptions
20from django.core.validators import URLValidator
21from django.http import Http404, HttpResponseGone
23import structlog
24from corsheaders.defaults import default_headers as default_cors_headers
26import InvenTree.backup
27from InvenTree.cache import get_cache_config, is_global_cache_enabled
28from InvenTree.config import get_boolean_setting, get_oidc_private_key, get_setting
29from InvenTree.ready import isInMainThread, isRunningBackup
30from InvenTree.sentry import default_sentry_dsn, init_sentry
31from InvenTree.version import checkMinPythonVersion, inventreeCommitHash
32from users.oauth2_scopes import oauth2_scopes
34from . import config
35from .setting import (
36 db_backend,
37 ldap,
38 locales,
39 markdown,
40 spectacular,
41 storages,
42 tracing,
43 worker,
44)
46try:
47 import django_stubs_ext
49 django_stubs_ext.monkeypatch() # pragma: no cover
50except ImportError: # pragma: no cover
51 pass
53checkMinPythonVersion()
55INVENTREE_BASE_URL = 'https://inventree.org'
56INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom'
58# Determine if we are running in "test" mode e.g. "manage.py test"
59TESTING = (
60 'test' in sys.argv
61 or 'TESTING' in os.environ
62 or any('pytest' in arg for arg in sys.argv)
63)
65if TESTING: 65 ↛ 67line 65 didn't jump to line 67 because the condition on line 65 was never true
66 # Use a weaker password hasher for testing (improves testing speed)
67 PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
69 # Enable slow-test-runner
70 TEST_RUNNER = 'django_slowtests.testrunner.DiscoverSlowestTestsRunner'
71 NUM_SLOW_TESTS = 25
74# Are environment variables manipulated by tests? Needs to be set by testing code
75TESTING_ENV = False
76# Are we bypassing exceptions?
77TESTING_BYPASS_MAILCHECK = False # Bypass email disablement for tests
79# New requirement for django 3.2+
80DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
82# Build paths inside the project like this: BASE_DIR.joinpath(...)
83BASE_DIR = config.get_base_dir()
85# Load configuration data
86CONFIG = config.load_config_data(set_cache=True)
87config.load_version_file()
89# Default action is to run the system in Debug mode
90# SECURITY WARNING: don't run with debug turned on in production!
91DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', False)
93# Internal flag to determine if we are running in docker mode
94DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False)
96AUTO_UPDATE = get_boolean_setting('INVENTREE_AUTO_UPDATE', 'auto_update', False)
98# Configure logging settings
99LOG_LEVEL = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
100JSON_LOG = get_boolean_setting('INVENTREE_JSON_LOG', 'json_log', False)
101WRITE_LOG = get_boolean_setting('INVENTREE_WRITE_LOG', 'write_log', False)
102CONSOLE_LOG = get_boolean_setting('INVENTREE_CONSOLE_LOG', 'console_log', True)
104logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s %(levelname)s %(message)s')
106if LOG_LEVEL not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 LOG_LEVEL = 'WARNING' # pragma: no cover
108DEFAULT_LOG_HANDLER = ['console'] if CONSOLE_LOG else []
109LOGGING = {
110 'version': 1,
111 'disable_existing_loggers': False,
112 'filters': {
113 'require_not_maintenance_mode_503': {
114 '()': 'maintenance_mode.logging.RequireNotMaintenanceMode503'
115 }
116 },
117 'formatters': {
118 'json_formatter': {
119 '()': structlog.stdlib.ProcessorFormatter,
120 'processor': structlog.processors.JSONRenderer(),
121 },
122 'plain_console': {
123 '()': structlog.stdlib.ProcessorFormatter,
124 'processor': structlog.dev.ConsoleRenderer(),
125 },
126 'key_value': {
127 '()': structlog.stdlib.ProcessorFormatter,
128 'processor': structlog.processors.KeyValueRenderer(
129 key_order=['timestamp', 'level', 'event', 'logger']
130 ),
131 },
132 },
133 'handlers': {
134 'console': {'class': 'logging.StreamHandler', 'formatter': 'plain_console'}
135 }
136 if CONSOLE_LOG
137 else {},
138 'loggers': {
139 'django_structlog': {'handlers': DEFAULT_LOG_HANDLER, 'level': LOG_LEVEL},
140 'inventree': {'handlers': DEFAULT_LOG_HANDLER, 'level': LOG_LEVEL},
141 },
142}
145# Add handlers
146if WRITE_LOG and JSON_LOG: # pragma: no cover 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 LOGGING['handlers']['log_file'] = {
148 'class': 'logging.handlers.WatchedFileHandler',
149 'filename': str(BASE_DIR.joinpath('logs.json')),
150 'formatter': 'json_formatter',
151 }
152 DEFAULT_LOG_HANDLER.append('log_file')
153elif WRITE_LOG: # pragma: no cover 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 LOGGING['handlers']['log_file'] = {
155 'class': 'logging.handlers.WatchedFileHandler',
156 'filename': str(BASE_DIR.joinpath('logs.log')),
157 'formatter': 'key_value',
158 }
159 DEFAULT_LOG_HANDLER.append('log_file')
161structlog.configure(
162 processors=[
163 structlog.contextvars.merge_contextvars,
164 structlog.stdlib.filter_by_level,
165 structlog.processors.TimeStamper(fmt='iso'),
166 structlog.stdlib.add_logger_name,
167 structlog.stdlib.add_log_level,
168 structlog.stdlib.PositionalArgumentsFormatter(),
169 structlog.processors.StackInfoRenderer(),
170 structlog.processors.format_exc_info,
171 structlog.processors.UnicodeDecoder(),
172 structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
173 ],
174 logger_factory=structlog.stdlib.LoggerFactory(),
175 cache_logger_on_first_use=True,
176)
177# Optionally add database-level logging
178if get_setting('INVENTREE_DB_LOGGING', 'db_logging', False): # pragma: no cover 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true
179 LOGGING['loggers'] = {'django.db.backends': {'level': LOG_LEVEL or 'DEBUG'}}
181# Get a logger instance for this setup file
182logger = structlog.getLogger('inventree')
184# Load SECRET_KEY
185SECRET_KEY = config.get_secret_key()
187# The filesystem location for served static files
188STATIC_ROOT = config.get_static_dir()
190# The filesystem location for uploaded media files
191MEDIA_ROOT = config.get_media_dir()
193# Needed for the parts importer, directly impacts the maximum parts that can be uploaded
194DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
196# Web URL endpoint for served static files
197STATIC_URL = '/static/'
199# Web URL endpoint for served media files
200MEDIA_URL = '/media/'
202# Are plugins enabled?
203PLUGINS_ENABLED = get_boolean_setting(
204 'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False
205)
207PLUGINS_MANDATORY = get_setting(
208 'INVENTREE_PLUGINS_MANDATORY', 'plugins_mandatory', typecast=list, default_value=[]
209)
211if PLUGINS_MANDATORY: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 logger.info('Mandatory plugins: %s', PLUGINS_MANDATORY)
214PLUGINS_INSTALL_DISABLED = get_boolean_setting(
215 'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False
216)
218if not PLUGINS_ENABLED: 218 ↛ 223line 218 didn't jump to line 223 because the condition on line 218 was always true
219 PLUGINS_INSTALL_DISABLED = (
220 True # If plugins are disabled, also disable installation
221 )
223PLUGIN_FILE = config.get_plugin_file()
225# Plugin test settings
226PLUGIN_TESTING = get_setting(
227 'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING
228) # Are plugins being tested?
230PLUGIN_TESTING_SETUP = get_setting(
231 'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False
232) # Load plugins from setup hooks in testing?
234PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
235PLUGIN_TESTING_EVENTS_ASYNC = False # Flag if events are tested asynchronously
236PLUGIN_TESTING_RELOAD = False # Flag if plugin reloading is in testing (check_reload)
238# Plugin development settings
239PLUGIN_DEV_SLUG = get_setting('INVENTREE_PLUGIN_DEV_SLUG', 'plugin_dev.slug')
241PLUGIN_DEV_HOST = get_setting(
242 'INVENTREE_PLUGIN_DEV_HOST', 'plugin_dev.host', 'http://localhost:5174'
243) # Host for the plugin development server
245PLUGIN_RETRY = get_setting(
246 'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int
247) # How often should plugin loading be tried?
249# Hash of the plugin file (will be updated on each change)
250PLUGIN_FILE_HASH = ''
252STATICFILES_DIRS = []
254# Append directory for compiled react files if debug server is running
255if DEBUG and 'collectstatic' not in sys.argv: 255 ↛ 256line 255 didn't jump to line 256 because the condition on line 255 was never true
256 web_dir = BASE_DIR.joinpath('..', 'web', 'static').absolute()
257 if web_dir.exists():
258 STATICFILES_DIRS.append(web_dir)
260 # Append directory for sample plugin static content (if in debug mode)
261 if PLUGINS_ENABLED:
262 logger.info('Adding plugin sample static content')
263 STATICFILES_DIRS.append(BASE_DIR.joinpath('plugin', 'samples', 'static'))
265# Database backup options
266# Ref: https://archmonger.github.io/django-dbbackup/latest/configuration/
268# For core backup functionality, refer to the STORAGES["dbbackup"] entry (below)
270DBBACKUP_DATE_FORMAT = InvenTree.backup.backup_date_format()
271DBBACKUP_FILENAME_TEMPLATE = InvenTree.backup.backup_filename_template()
272DBBACKUP_MEDIA_FILENAME_TEMPLATE = InvenTree.backup.backup_media_filename_template()
274DBBACKUP_GPG_RECIPIENT = InvenTree.backup.backup_gpg_recipient()
276DBBACKUP_SEND_EMAIL = InvenTree.backup.backup_email_on_error()
277DBBACKUP_EMAIL_SUBJECT_PREFIX = InvenTree.backup.backup_email_prefix()
279DBBACKUP_CONNECTORS = {'default': InvenTree.backup.get_backup_connector_options()}
281DBBACKUP_BACKUP_METADATA_SETTER = InvenTree.backup.metadata_set
282DBBACKUP_RESTORE_METADATA_VALIDATOR = InvenTree.backup.validate_restore
284# Data storage options
285DBBACKUP_STORAGE_CONFIG = {
286 'BACKEND': InvenTree.backup.get_backup_storage_backend(),
287 'OPTIONS': InvenTree.backup.get_backup_storage_options(),
288}
290# This can also be overridden with a command line flag --restore-allow-newer-version when running the restore command
291BACKUP_RESTORE_ALLOW_NEWER_VERSION = get_boolean_setting(
292 'INVENTREE_BACKUP_RESTORE_ALLOW_NEWER_VERSION',
293 'backup_restore_allow_newer_version',
294 False,
295)
297# Enable django admin interface?
298INVENTREE_ADMIN_ENABLED = get_boolean_setting(
299 'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True
300)
302# Base URL for admin pages (default="admin")
303INVENTREE_ADMIN_URL = get_setting(
304 'INVENTREE_ADMIN_URL', config_key='admin_url', default_value='admin'
305)
307INSTALLED_APPS = [
308 # Admin site integration
309 'django.contrib.admin',
310 'django.contrib.admindocs',
311 # InvenTree apps
312 'build.apps.BuildConfig',
313 'common.apps.CommonConfig',
314 'plugin.apps.PluginAppConfig', # Plugin app runs before all apps that depend on the isPluginRegistryLoaded function
315 'company.apps.CompanyConfig',
316 'order.apps.OrderConfig',
317 'part.apps.PartConfig',
318 'report.apps.ReportConfig',
319 'stock.apps.StockConfig',
320 'users.apps.UsersConfig',
321 'machine.apps.MachineConfig',
322 'data_exporter.apps.DataExporterConfig',
323 'importer.apps.ImporterConfig',
324 'web',
325 'generic',
326 'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
327 # Core django modules
328 'django.contrib.auth',
329 'django.contrib.contenttypes',
330 'django.contrib.sessions',
331 'django.contrib.humanize',
332 'whitenoise.runserver_nostatic',
333 'django.contrib.messages',
334 'django.contrib.staticfiles',
335 'django.contrib.sites',
336 # Maintenance
337 'maintenance_mode',
338 # Third part add-ons
339 'django_filters', # Extended filter functionality
340 'rest_framework', # DRF (Django Rest Framework)
341 'corsheaders', # Cross-origin Resource Sharing for DRF
342 'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
343 'mptt', # Modified Preorder Tree Traversal
344 'markdownify', # Markdown template rendering
345 'djmoney', # django-money integration
346 'djmoney.contrib.exchange', # django-money exchange rates
347 'error_report', # Error reporting in the admin interface
348 'django_q',
349 'dbbackup', # Backups - django-dbbackup
350 'taggit', # Tagging
351 'flags', # Flagging - django-flags
352 'django_structlog', # Structured logging
353 'allauth', # Base app for SSO
354 'allauth.account', # Extend user with accounts
355 'allauth.headless', # APIs for auth
356 'allauth.socialaccount', # Use 'social' providers
357 'allauth.mfa', # MFA for for allauth
358 'allauth.usersessions', # DB sessions
359 'django_otp', # OTP is needed for MFA - base package
360 'django_otp.plugins.otp_totp', # Time based OTP
361 'django_otp.plugins.otp_static', # Backup codes
362 'oauth2_provider', # OAuth2 provider and API access
363 'drf_spectacular', # API documentation
364 'django_ical', # For exporting calendars
365 'django_mailbox', # For email import
366 'anymail', # For email sending/receiving via ESPs
367 'storages',
368]
370MIDDLEWARE = CONFIG.get(
371 'middleware',
372 [
373 'django.middleware.security.SecurityMiddleware',
374 'x_forwarded_for.middleware.XForwardedForMiddleware',
375 'django.contrib.sessions.middleware.SessionMiddleware',
376 'allauth.usersessions.middleware.UserSessionsMiddleware', # DB user sessions
377 'django.middleware.locale.LocaleMiddleware',
378 'django.middleware.csrf.CsrfViewMiddleware',
379 'corsheaders.middleware.CorsMiddleware',
380 'whitenoise.middleware.WhiteNoiseMiddleware',
381 'django.middleware.common.CommonMiddleware',
382 'django.contrib.auth.middleware.AuthenticationMiddleware',
383 'InvenTree.middleware.InvenTreeRemoteUserMiddleware', # Remote / proxy auth
384 'allauth.account.middleware.AccountMiddleware',
385 'django.contrib.messages.middleware.MessageMiddleware',
386 'django.middleware.clickjacking.XFrameOptionsMiddleware',
387 'InvenTree.middleware.AuthRequiredMiddleware',
388 'InvenTree.middleware.Check2FAMiddleware', # Check if the user should be forced to use MFA
389 'oauth2_provider.middleware.OAuth2TokenMiddleware', # oauth2_provider
390 'maintenance_mode.middleware.MaintenanceModeMiddleware',
391 'InvenTree.middleware.InvenTreeExceptionProcessor', # Error reporting
392 'InvenTree.middleware.InvenTreeRequestCacheMiddleware', # Request caching
393 'InvenTree.middleware.InvenTreeHostSettingsMiddleware', # Ensuring correct hosting/security settings
394 'django_structlog.middlewares.RequestMiddleware', # Structured logging
395 'InvenTree.middleware.InvenTreeVersionHeaderMiddleware',
396 ],
397)
399# In DEBUG mode, add support for django-silk
400# Ref: https://silk.readthedocs.io/en/latest/
401DJANGO_SILK_ENABLED = (
402 get_boolean_setting( # pragma: no cover
403 'INVENTREE_DEBUG_SILK', 'debug_silk', False
404 )
405 and DEBUG
406)
408if DJANGO_SILK_ENABLED: # pragma: no cover 408 ↛ 409line 408 didn't jump to line 409 because the condition on line 408 was never true
409 MIDDLEWARE.append('silk.middleware.SilkyMiddleware')
410 INSTALLED_APPS.append('silk')
412 # Optionally enable the silk python profiler
413 SILKY_PYTHON_PROFILER = SILKY_PYTHON_PROFILER_BINARY = get_boolean_setting(
414 'INVENTREE_DEBUG_SILK_PROFILING', 'debug_silk_profiling', False
415 )
417# In DEBUG mode, add support for django-querycount
418# Ref: https://github.com/bradmontgomery/django-querycount
419if ( 419 ↛ 425line 419 didn't jump to line 425 because the condition on line 419 was never true
420 get_boolean_setting( # pragma: no cover
421 'INVENTREE_DEBUG_QUERYCOUNT', 'debug_querycount', False
422 )
423 and DEBUG
424):
425 MIDDLEWARE.append('querycount.middleware.QueryCountMiddleware')
426 logger.debug('Running with debug_querycount middleware enabled')
428QUERYCOUNT = {
429 'THRESHOLDS': {
430 'MEDIUM': 50,
431 'HIGH': 200,
432 'MIN_TIME_TO_LOG': 0.1,
433 'MIN_QUERY_COUNT_TO_LOG': 25,
434 },
435 'IGNORE_REQUEST_PATTERNS': [r'^(?!\/(api)?(plugin)?\/).*'],
436 'IGNORE_SQL_PATTERNS': [],
437 'DISPLAY_DUPLICATES': 1,
438 'RESPONSE_HEADER': 'X-Django-Query-Count',
439}
442default_auth_backends = [
443 'oauth2_provider.backends.OAuth2Backend', # OAuth2 provider
444 'django.contrib.auth.backends.RemoteUserBackend', # proxy login
445 'django.contrib.auth.backends.ModelBackend',
446 'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
447 'sesame.backends.ModelBackend', # Magic link login django-sesame
448]
450AUTHENTICATION_BACKENDS = (
451 CONFIG.get('authentication_backends', default_auth_backends)
452 if CONFIG
453 else default_auth_backends
454)
456# LDAP support
457LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False)
458LDAP_DEBUG = (
459 get_boolean_setting('INVENTREE_LDAP_DEBUG', 'ldap.debug', False) and LDAP_AUTH
460)
462if LDAP_AUTH: # pragma: no cover 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true
463 AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
465 # debug mode to troubleshoot configuration
466 if LDAP_DEBUG:
467 if 'loggers' not in LOGGING:
468 LOGGING['loggers'] = {}
469 LOGGING['loggers']['django_auth_ldap'] = {
470 'level': 'DEBUG',
471 'handlers': DEFAULT_LOG_HANDLER,
472 }
474 # Determine LDAP settings
475 ldap_settings = ldap.get_ldap_config(debug=LDAP_DEBUG)
476 globals().update(ldap_settings)
479# Allow secure http developer server in debug mode
480if DEBUG: 480 ↛ 481line 480 didn't jump to line 481 because the condition on line 480 was never true
481 INSTALLED_APPS.append('sslserver')
483# InvenTree URL configuration
484ROOT_URLCONF = 'InvenTree.urls'
486TEMPLATES = [
487 {
488 'BACKEND': 'django.template.backends.django.DjangoTemplates',
489 'DIRS': [BASE_DIR.joinpath('templates'), MEDIA_ROOT.joinpath('report')],
490 'OPTIONS': {
491 'context_processors': [
492 'django.template.context_processors.debug',
493 'django.template.context_processors.request',
494 'django.template.context_processors.i18n',
495 'django.contrib.auth.context_processors.auth',
496 'django.contrib.messages.context_processors.messages',
497 ],
498 'loaders': [
499 (
500 'InvenTree.template.InvenTreeTemplateLoader',
501 [
502 'plugin.template.PluginTemplateLoader',
503 'django.template.loaders.filesystem.Loader',
504 'django.template.loaders.app_directories.Loader',
505 ],
506 )
507 ],
508 },
509 }
510]
512REST_FRAMEWORK = {
513 'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
514 'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
515 'DEFAULT_AUTHENTICATION_CLASSES': [
516 'users.authentication.ApiTokenAuthentication',
517 'rest_framework.authentication.BasicAuthentication',
518 'rest_framework.authentication.SessionAuthentication',
519 'users.authentication.ExtendedOAuth2Authentication',
520 ],
521 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
522 'DEFAULT_PERMISSION_CLASSES': [
523 'rest_framework.permissions.IsAuthenticated',
524 'InvenTree.permissions.ModelPermission',
525 'InvenTree.permissions.RolePermission',
526 'InvenTree.permissions.InvenTreeTokenMatchesOASRequirements',
527 ],
528 'DEFAULT_SCHEMA_CLASS': 'InvenTree.schema.ExtendedAutoSchema',
529 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
530 'DEFAULT_RENDERER_CLASSES': ['rest_framework.renderers.JSONRenderer'],
531 'TOKEN_MODEL': 'users.models.ApiToken',
532}
534if DEBUG: 534 ↛ 536line 534 didn't jump to line 536 because the condition on line 534 was never true
535 # Enable browsable API if in DEBUG mode
536 REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append(
537 'rest_framework.renderers.BrowsableAPIRenderer'
538 )
540# JWT settings - rest_framework_simplejwt
541USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False)
542if USE_JWT: 542 ↛ 543line 542 didn't jump to line 543 because the condition on line 542 was never true
543 JWT_AUTH_COOKIE = 'inventree-auth'
544 JWT_AUTH_REFRESH_COOKIE = 'inventree-token'
545 INSTALLED_APPS.append('rest_framework_simplejwt')
547# WSGI default setting
548WSGI_APPLICATION = 'InvenTree.wsgi.application'
550"""
551Configure the database backend based on the user-specified values.
553- Primarily this configuration happens in the config.yaml file
554- However there may be reason to configure the DB via environmental variables
555- The following code lets the user "mix and match" database configuration
556"""
558logger.debug('Configuring database backend:')
560# Load database configuration from the config file and environment variables
561database = db_backend.get_db_backend()
562DB_ENGINE = database['ENGINE']
564DATABASES = {'default': database}
566# login settings
567REMOTE_LOGIN = get_boolean_setting(
568 'INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False
569)
570REMOTE_LOGIN_HEADER = get_setting(
571 'INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER'
572)
574# region Tracing / error tracking
575inventree_tags = {
576 'testing': TESTING,
577 'docker': DOCKER,
578 'debug': DEBUG,
579 'remote': REMOTE_LOGIN,
580 'commit': inventreeCommitHash(),
581}
583# sentry.io integration for error reporting
584SENTRY_ENABLED = not TESTING and get_boolean_setting(
585 'INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False
586)
588# Default Sentry DSN (can be overridden if user wants custom sentry integration)
589SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', default_sentry_dsn())
590SENTRY_SAMPLE_RATE = float(
591 get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1)
592)
594if SENTRY_ENABLED and SENTRY_DSN and not TESTING: # pragma: no cover 594 ↛ 595line 594 didn't jump to line 595 because the condition on line 594 was never true
595 init_sentry(SENTRY_DSN, SENTRY_SAMPLE_RATE, inventree_tags)
597# OpenTelemetry tracing
598TRACING_ENABLED = (
599 get_boolean_setting('INVENTREE_TRACING_ENABLED', 'tracing.enabled', False)
600 and not isRunningBackup()
601)
603TRACING_DETAILS: Optional[dict] = tracing.configure_tracing(
604 DB_ENGINE, TRACING_ENABLED, inventree_tags
605)
607# Cache configuration
608GLOBAL_CACHE_ENABLED = is_global_cache_enabled()
610CACHES = {'default': get_cache_config(GLOBAL_CACHE_ENABLED)}
612# Background task processing with django-q
613Q_CLUSTER = worker.get_worker_config(
614 DB_ENGINE,
615 global_cache=GLOBAL_CACHE_ENABLED,
616 sentry_dsn=SENTRY_DSN if SENTRY_ENABLED and SENTRY_DSN else None,
617 debug=DEBUG,
618)
620SILENCED_SYSTEM_CHECKS = ['templates.E003', 'templates.W003']
622# Password validation
623# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
625AUTH_PASSWORD_VALIDATORS = [
626 {
627 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'
628 },
629 {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
630 {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
631 {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
632]
634# Extra (optional) URL validators
635# See https://docs.djangoproject.com/en/2.2/ref/validators/#django.core.validators.URLValidator
637EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', [])
639if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true
640 logger.warning('extra_url_schemes not correctly formatted')
641 EXTRA_URL_SCHEMES = []
643LANGUAGES = locales.LOCALES
645LOCALE_CODES = [lang[0] for lang in LANGUAGES]
647# Internationalization
648# https://docs.djangoproject.com/en/dev/topics/i18n/
649LANGUAGE_CODE = get_setting('INVENTREE_LANGUAGE', 'language', 'en-us')
651if ( 651 ↛ 655line 651 didn't jump to line 655 because the condition on line 651 was never true
652 LANGUAGE_CODE not in LOCALE_CODES
653 and LANGUAGE_CODE.split('-')[0] not in LOCALE_CODES
654): # pragma: no cover
655 logger.warning(
656 'Language code %s not supported - defaulting to en-us', LANGUAGE_CODE
657 )
658 LANGUAGE_CODE = 'en-us'
660# Store language settings for 30 days
661LANGUAGE_COOKIE_AGE = 2592000
663# Testing interface translations
664if get_boolean_setting('TEST_TRANSLATIONS', default_value=False): # pragma: no cover 664 ↛ 666line 664 didn't jump to line 666 because the condition on line 664 was never true
665 # Set default language
666 LANGUAGE_CODE = 'xx'
668 # Add to language catalog
669 LANGUAGES.append(('xx', 'Test'))
671 # Add custom languages not provided by Django
672 EXTRA_LANG_INFO = {'xx': {'code': 'xx', 'name': 'Test', 'name_local': 'Test'}}
673 LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
674 django.conf.locale.LANG_INFO = LANG_INFO
676# Maximum number of decimal places for currency rendering
677CURRENCY_DECIMAL_PLACES = 6
679# Custom currency exchange backend
680EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
682# region email
683# Email configuration options
684EMAIL_BACKEND = 'InvenTree.backends.InvenTreeMailLoggingBackend'
686INTERNAL_EMAIL_BACKEND = get_setting(
687 'INVENTREE_EMAIL_BACKEND',
688 'email.backend',
689 'django.core.mail.backends.smtp.EmailBackend',
690)
692# SMTP backend
693EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host')
694EMAIL_PORT = get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25, typecast=int)
695EMAIL_HOST_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username')
696EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password')
697EMAIL_USE_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
698EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
699# Anymail
700if INTERNAL_EMAIL_BACKEND.startswith('anymail.backends.'): 700 ↛ 701line 700 didn't jump to line 701 because the condition on line 700 was never true
701 ANYMAIL = get_setting('INVENTREE_ANYMAIL', 'email.anymail', None, dict)
703EMAIL_SUBJECT_PREFIX = get_setting(
704 'INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] '
705)
706DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
708# If "from" email not specified, default to the username
709if not DEFAULT_FROM_EMAIL: 709 ↛ 712line 709 didn't jump to line 712 because the condition on line 709 was always true
710 DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
712EMAIL_USE_LOCALTIME = False
713EMAIL_TIMEOUT = 60
714# endregion email
716LOCALE_PATHS = (BASE_DIR.joinpath('locale/'),)
718TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
720# Check that the timezone is valid
721try:
722 ZoneInfo(TIME_ZONE)
723except ZoneInfoNotFoundError: # pragma: no cover
724 raise ValueError(f"Specified timezone '{TIME_ZONE}' is not valid")
726USE_I18N = True
728# Do not use native timezone support in "test" mode
729# It generates a *lot* of cruft in the logs
730USE_TZ = bool(not TESTING)
732DATE_INPUT_FORMATS = ['%Y-%m-%d']
734# Site URL can be specified statically, or via a run-time setting
735SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
736SITE_LAX_PROTOCOL_CHECK = get_boolean_setting(
737 'INVENTREE_SITE_LAX_PROTOCOL', 'site_lax_protocol', True
738)
740if SITE_URL: 740 ↛ 755line 740 didn't jump to line 755 because the condition on line 740 was always true
741 SITE_URL = str(SITE_URL).strip().rstrip('/')
742 logger.info('Using Site URL: %s', SITE_URL)
744 # Check that the site URL is valid
745 try:
746 validator = URLValidator()
747 validator(SITE_URL)
748 except Exception:
749 msg = f"Invalid SITE_URL value: '{SITE_URL}'. InvenTree server cannot start."
750 logger.error(msg)
751 print(msg)
752 sys.exit(-1)
754else:
755 logger.warning('No SITE_URL specified. Some features may not work correctly')
756 logger.warning(
757 'Specify a SITE_URL in the configuration file or via an environment variable'
758 )
760# Enable or disable multi-site framework
761SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False)
763# If a SITE_ID is specified
764SITE_ID = get_setting('INVENTREE_SITE_ID', 'site_id', 1 if SITE_MULTI else None)
766# Load the allauth social backends
767SOCIAL_BACKENDS = get_setting(
768 'INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list
769)
771if not SITE_MULTI: 771 ↛ 776line 771 didn't jump to line 776 because the condition on line 771 was always true
772 INSTALLED_APPS.remove('django.contrib.sites')
774# List of allowed hosts (default = allow all)
775# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts
776ALLOWED_HOSTS = get_setting(
777 'INVENTREE_ALLOWED_HOSTS',
778 config_key='allowed_hosts',
779 default_value=[],
780 typecast=list,
781)
783if SITE_URL and SITE_URL not in ALLOWED_HOSTS: 783 ↛ 786line 783 didn't jump to line 786 because the condition on line 783 was always true
784 ALLOWED_HOSTS.append(SITE_URL)
786if not ALLOWED_HOSTS: 786 ↛ 787line 786 didn't jump to line 787 because the condition on line 786 was never true
787 if DEBUG:
788 logger.info(
789 'No ALLOWED_HOSTS specified. Defaulting to ["*"] for debug mode. This is not recommended for production use'
790 )
791 ALLOWED_HOSTS = ['*']
792 elif not TESTING:
793 logger.error(
794 'No ALLOWED_HOSTS specified. Please provide a list of allowed hosts, or specify INVENTREE_SITE_URL'
795 )
797 # Server cannot run without ALLOWED_HOSTS
798 if isInMainThread():
799 sys.exit(-1)
801# Ensure that the ALLOWED_HOSTS do not contain any scheme info
802for i, host in enumerate(ALLOWED_HOSTS):
803 if '://' in host:
804 ALLOWED_HOSTS[i] = host = host.split('://')[1]
806 if ':' in host:
807 ALLOWED_HOSTS[i] = host = host.split(':')[0]
809# List of trusted origins for unsafe requests
810# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
811CSRF_TRUSTED_ORIGINS = get_setting(
812 'INVENTREE_TRUSTED_ORIGINS',
813 config_key='trusted_origins',
814 default_value=[],
815 typecast=list,
816)
818# If a list of trusted is not specified, but a site URL has been specified, use that
819if SITE_URL and SITE_URL not in CSRF_TRUSTED_ORIGINS: 819 ↛ 822line 819 didn't jump to line 822 because the condition on line 819 was always true
820 CSRF_TRUSTED_ORIGINS.append(SITE_URL)
822if DEBUG: 822 ↛ 823line 822 didn't jump to line 823 because the condition on line 822 was never true
823 for origin in [
824 'http://localhost',
825 'http://*.localhost',
826 'http://*localhost:8000',
827 'http://*localhost:4173',
828 'http://*localhost:5173',
829 ]:
830 if origin not in CSRF_TRUSTED_ORIGINS:
831 CSRF_TRUSTED_ORIGINS.append(origin)
833if ( 833 ↛ 837line 833 didn't jump to line 837 because the condition on line 833 was never true
834 not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0 and isInMainThread()
835): # pragma: no cover
836 # Server thread cannot run without CSRF_TRUSTED_ORIGINS
837 logger.error(
838 'No CSRF_TRUSTED_ORIGINS specified. Please provide a list of trusted origins, or specify INVENTREE_SITE_URL'
839 )
840 sys.exit(-1)
842COOKIE_MODE = (
843 str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False'))
844 .lower()
845 .strip()
846)
848# Valid modes (as per the django settings documentation)
849valid_cookie_modes = ['lax', 'strict', 'none']
851COOKIE_MODE = COOKIE_MODE.capitalize() if COOKIE_MODE in valid_cookie_modes else False
853# Additional CSRF settings
854CSRF_FAILURE_VIEW = 'InvenTree.middleware.csrf_failure'
855CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
856CSRF_COOKIE_NAME = 'csrftoken'
858CSRF_COOKIE_SAMESITE = COOKIE_MODE
859SESSION_COOKIE_SAMESITE = COOKIE_MODE
860LANGUAGE_COOKIE_SAMESITE = COOKIE_MODE
862"""Set the SESSION_COOKIE_SECURE value based on the following rules:
863- False if the server is running in DEBUG mode
864- True if samesite cookie setting is set to 'None'
865- Otherwise, use the value specified in the configuration file (or env var)
866"""
867COOKIE_SECURE = (
868 get_boolean_setting('INVENTREE_SESSION_COOKIE_SECURE', 'cookie.secure', False)
869 or SESSION_COOKIE_SAMESITE == 'None'
870)
872# Override COOKIE_SECURE value in DEBUG mode
873if DEBUG: 873 ↛ 874line 873 didn't jump to line 874 because the condition on line 873 was never true
874 COOKIE_SECURE = False
876CSRF_COOKIE_SECURE = COOKIE_SECURE
877SESSION_COOKIE_SECURE = COOKIE_SECURE
878LANGUAGE_COOKIE_SECURE = COOKIE_SECURE
880# Ref: https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-SECURE_PROXY_SSL_HEADER
881if ssl_header := get_boolean_setting( 881 ↛ 885line 881 didn't jump to line 885 because the condition on line 881 was never true
882 'INVENTREE_USE_X_FORWARDED_PROTO', 'use_x_forwarded_proto', False
883):
884 # The default header name is 'HTTP_X_FORWARDED_PROTO', but can be adjusted
885 ssl_header_name = get_setting(
886 'INVENTREE_X_FORWARDED_PROTO_NAME',
887 'x_forwarded_proto_name',
888 'HTTP_X_FORWARDED_PROTO',
889 )
890 SECURE_PROXY_SSL_HEADER = (ssl_header_name, 'https')
892USE_X_FORWARDED_HOST = get_boolean_setting(
893 'INVENTREE_USE_X_FORWARDED_HOST',
894 config_key='use_x_forwarded_host',
895 default_value=False,
896)
898USE_X_FORWARDED_PORT = get_boolean_setting(
899 'INVENTREE_USE_X_FORWARDED_PORT',
900 config_key='use_x_forwarded_port',
901 default_value=False,
902)
904# Cross Origin Resource Sharing (CORS) options
905# Refer to the django-cors-headers documentation for more information
906# Ref: https://github.com/adamchainz/django-cors-headers
909# Extract CORS options from configuration file
910CORS_ALLOW_ALL_ORIGINS = get_boolean_setting(
911 'INVENTREE_CORS_ORIGIN_ALLOW_ALL', config_key='cors.allow_all', default_value=DEBUG
912)
914CORS_ALLOW_CREDENTIALS = get_boolean_setting(
915 'INVENTREE_CORS_ALLOW_CREDENTIALS',
916 config_key='cors.allow_credentials',
917 default_value=True,
918)
920# Only allow CORS access to the following URL endpoints
921CORS_URLS_REGEX = r'^/(api|auth|media|plugin|static)/.*$'
923CORS_ALLOWED_ORIGINS = get_setting(
924 'INVENTREE_CORS_ORIGIN_WHITELIST',
925 config_key='cors.whitelist',
926 default_value=[],
927 typecast=list,
928)
930# If no CORS origins are specified, but a site URL has been specified, use that
931if SITE_URL and SITE_URL not in CORS_ALLOWED_ORIGINS: 931 ↛ 934line 931 didn't jump to line 934 because the condition on line 931 was always true
932 CORS_ALLOWED_ORIGINS.append(SITE_URL)
934CORS_ALLOWED_ORIGIN_REGEXES = get_setting(
935 'INVENTREE_CORS_ORIGIN_REGEX',
936 config_key='cors.regex',
937 default_value=[],
938 typecast=list,
939)
941_allowed_headers = (*default_cors_headers, 'traceparent')
942# Allow extra CORS headers in DEBUG mode
943# Required for serving /static/ and /media/ files
944if DEBUG: 944 ↛ 945line 944 didn't jump to line 945 because the condition on line 944 was never true
945 _allowed_headers = (*_allowed_headers, 'cache-control', 'pragma', 'expires')
946CORS_ALLOW_HEADERS = _allowed_headers
948# In debug mode allow CORS requests from localhost
949# This allows connection from the frontend development server
950if DEBUG: 950 ↛ 951line 950 didn't jump to line 951 because the condition on line 950 was never true
951 CORS_ALLOWED_ORIGIN_REGEXES.append(r'^http://localhost:\d+$')
953if CORS_ALLOW_ALL_ORIGINS: 953 ↛ 956line 953 didn't jump to line 956 because the condition on line 953 was always true
954 logger.info('CORS: All origins allowed')
955else:
956 if CORS_ALLOWED_ORIGINS:
957 logger.info('CORS: Whitelisted origins: %s', CORS_ALLOWED_ORIGINS)
959 if CORS_ALLOWED_ORIGIN_REGEXES:
960 logger.info('CORS: Whitelisted origin regexes: %s', CORS_ALLOWED_ORIGIN_REGEXES)
962# Load settings for the frontend interface
963FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
964FRONTEND_URL_BASE = FRONTEND_SETTINGS['base_url']
966# region auth
967for app in SOCIAL_BACKENDS: # pragma: no cover 967 ↛ 969line 967 didn't jump to line 969 because the loop on line 967 never started
968 # Ensure that the app starts with 'allauth.socialaccount.providers'
969 social_prefix = 'allauth.socialaccount.providers.'
971 if not app.startswith(social_prefix):
972 app = social_prefix + app
974 INSTALLED_APPS.append(app)
976SOCIALACCOUNT_PROVIDERS = get_setting(
977 'INVENTREE_SOCIAL_PROVIDERS', 'social_providers', None, typecast=dict
978)
980SOCIALACCOUNT_STORE_TOKENS = True
982# Explicitly set empty URL prefix for OIDC
983# The SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX setting was introduced in v0.60.0
984# Ref: https://github.com/pennersr/django-allauth/blob/0.60.0/ChangeLog.rst#backwards-incompatible-changes
985SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX = ''
987# settings for allauth
988ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting(
989 'INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3, typecast=int
990)
991ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
992ACCOUNT_EMAIL_NOTIFICATIONS = True
993USERSESSIONS_TRACK_ACTIVITY = True
995# allauth rate limiting: https://docs.allauth.org/en/latest/account/rate_limits.html
996# The default login rate limit is "5/m/user,5/m/ip,5/m/key"
997login_attempts = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5)
999try:
1000 login_attempts = int(login_attempts)
1001 login_attempts = f'{login_attempts}/m,{login_attempts}/m'
1002except ValueError: # pragma: no cover
1003 pass
1005ACCOUNT_RATE_LIMITS = {'login_failed': login_attempts}
1007# Default protocol for login
1008ACCOUNT_DEFAULT_HTTP_PROTOCOL = get_setting(
1009 'INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL', 'login_default_protocol', None
1010)
1012if ACCOUNT_DEFAULT_HTTP_PROTOCOL is None: 1012 ↛ 1013line 1012 didn't jump to line 1013 because the condition on line 1012 was never true
1013 if SITE_URL and SITE_URL.startswith('https://'):
1014 # auto-detect HTTPS protocol
1015 ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'https'
1016 else:
1017 # default to http
1018 ACCOUNT_DEFAULT_HTTP_PROTOCOL = 'http'
1020ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
1021ACCOUNT_PREVENT_ENUMERATION = True
1022ACCOUNT_EMAIL_SUBJECT_PREFIX = EMAIL_SUBJECT_PREFIX
1023# 2FA
1024REMOVE_SUCCESS_URL = 'settings'
1026# override forms / adapters
1027ACCOUNT_FORMS = {
1028 'login': 'InvenTree.auth_overrides.CustomLoginForm',
1029 'signup': 'InvenTree.auth_overrides.CustomSignupForm',
1030 'add_email': 'allauth.account.forms.AddEmailForm',
1031 'change_password': 'allauth.account.forms.ChangePasswordForm',
1032 'set_password': 'allauth.account.forms.SetPasswordForm',
1033 'reset_password': 'allauth.account.forms.ResetPasswordForm',
1034 'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
1035 'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
1036}
1038SOCIALACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomSocialAccountAdapter'
1039ACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomAccountAdapter'
1040HEADLESS_ADAPTER = 'InvenTree.auth_overrides.CustomHeadlessAdapter'
1041ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
1043HEADLESS_ONLY = True
1044HEADLESS_CLIENTS = 'browser'
1045MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True)
1047if not MFA_ENABLED: 1047 ↛ 1048line 1047 didn't jump to line 1048 because the condition on line 1047 was never true
1048 MIDDLEWARE.remove('InvenTree.middleware.Check2FAMiddleware')
1050MFA_SUPPORTED_TYPES = (
1051 get_setting(
1052 'INVENTREE_MFA_SUPPORTED_TYPES',
1053 'mfa_supported_types',
1054 ['totp', 'recovery_codes', 'webauthn'],
1055 typecast=list,
1056 )
1057 if MFA_ENABLED
1058 else []
1059)
1061MFA_TRUST_ENABLED = True
1062MFA_PASSKEY_LOGIN_ENABLED = True
1063MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG
1065LOGOUT_REDIRECT_URL = get_setting(
1066 'INVENTREE_LOGOUT_REDIRECT_URL', 'logout_redirect_url', 'index'
1067)
1068# endregion auth
1070# Markdownify configuration
1071# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
1073MARKDOWNIFY = markdown.markdownify_config()
1075# Ignore these error types for in-database error logging
1076IGNORED_ERRORS = [Http404, HttpResponseGone, django.core.exceptions.PermissionDenied]
1078# Maintenance mode
1079MAINTENANCE_MODE_RETRY_AFTER = 10
1080MAINTENANCE_MODE_STATE_BACKEND = 'InvenTree.backends.InvenTreeMaintenanceModeBackend'
1082# Flag to allow table events during testing
1083TESTING_TABLE_EVENTS = False
1085# Flag to allow pricing recalculations during testing
1086TESTING_PRICING = False
1088# Global settings overrides
1089# If provided, these values will override any "global" settings (and prevent them from being set)
1090GLOBAL_SETTINGS_OVERRIDES = get_setting(
1091 'INVENTREE_GLOBAL_SETTINGS', 'global_settings', typecast=dict
1092)
1094# Override site URL setting
1095if SITE_URL: 1095 ↛ 1098line 1095 didn't jump to line 1098 because the condition on line 1095 was always true
1096 GLOBAL_SETTINGS_OVERRIDES['INVENTREE_BASE_URL'] = SITE_URL
1098if len(GLOBAL_SETTINGS_OVERRIDES) > 0: 1098 ↛ 1107line 1098 didn't jump to line 1107 because the condition on line 1098 was always true
1099 logger.info('INVE-I1: Global settings overrides: %s', GLOBAL_SETTINGS_OVERRIDES)
1100 for key in GLOBAL_SETTINGS_OVERRIDES:
1101 # Set the global setting
1102 logger.debug('- Override value for %s = ********', key)
1104# Plugin settings overrides
1105# If provided, these values will override any plugin settings (and prevent them from being changed)
1106# Specified as a nested dict: {plugin_slug: {SETTING_KEY: value}}
1107PLUGIN_SETTING_OVERRIDES = get_setting(
1108 'INVENTREE_PLUGIN_SETTINGS', 'plugin_settings', typecast=dict
1109)
1111if len(PLUGIN_SETTING_OVERRIDES) > 0: 1111 ↛ 1112line 1111 didn't jump to line 1112 because the condition on line 1111 was never true
1112 logger.info(
1113 'INVE-I1: Plugin settings overrides: %s', list(PLUGIN_SETTING_OVERRIDES.keys())
1114 )
1115 for slug, overrides in PLUGIN_SETTING_OVERRIDES.items():
1116 for key in overrides:
1117 logger.debug('- Override value for %s.%s = ********', slug, key)
1119# User interface customization values
1120CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', typecast=str)
1122CUSTOM_SPLASH = get_setting('INVENTREE_CUSTOM_SPLASH', 'customize.splash', typecast=str)
1124CUSTOMIZE = get_setting(
1125 'INVENTREE_CUSTOMIZE', 'customize', default_value=None, typecast=dict
1126)
1128if DEBUG: 1128 ↛ 1129line 1128 didn't jump to line 1129 because the condition on line 1128 was never true
1129 logger.info('InvenTree running with DEBUG enabled')
1131logger.info("MEDIA_ROOT: '%s'", MEDIA_ROOT)
1132logger.info("STATIC_ROOT: '%s'", STATIC_ROOT)
1134# Flags
1135FLAGS = {
1136 'EXPERIMENTAL': [
1137 {'condition': 'boolean', 'value': DEBUG},
1138 {'condition': 'parameter', 'value': 'experimental='},
1139 ], # Should experimental features be turned on?
1140 'NEXT_GEN': [
1141 {'condition': 'parameter', 'value': 'ngen='}
1142 ], # Should next-gen features be turned on?
1143 'OIDC': [{'condition': 'parameter', 'value': 'oidc='}],
1144}
1146# Get custom flags from environment/yaml
1147CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
1148if CUSTOM_FLAGS: # pragma: no cover 1148 ↛ 1149line 1148 didn't jump to line 1149 because the condition on line 1148 was never true
1149 if not isinstance(CUSTOM_FLAGS, dict):
1150 logger.error('Invalid custom flags, must be valid dict: %s', CUSTOM_FLAGS)
1151 else:
1152 logger.info('Custom flags: %s', CUSTOM_FLAGS)
1153 FLAGS.update(CUSTOM_FLAGS)
1155# Magic login django-sesame
1156SESAME_MAX_AGE = 300
1157LOGIN_REDIRECT_URL = '/api/auth/login-redirect/'
1159# Configuration for API schema generation / oAuth2
1160SPECTACULAR_SETTINGS = spectacular.get_spectacular_settings()
1161SCHEMA_VENDOREXTENSION_LEVEL = get_setting(
1162 'INVENTREE_SCHEMA_LEVEL', 'schema.level', default_value=0, typecast=int
1163)
1165OAUTH2_PROVIDER = {
1166 # default scopes
1167 'SCOPES': oauth2_scopes,
1168 # OIDC
1169 'OIDC_ENABLED': True,
1170 'OIDC_RSA_PRIVATE_KEY': get_oidc_private_key(),
1171 'PKCE_REQUIRED': False,
1172}
1173OAUTH2_CHECK_EXCLUDED = [ # This setting mutes schema checks for these rule/method combinations
1174 '/api/email/generate/:post',
1175 '/api/webhook/{endpoint}/:post',
1176]
1178if SITE_URL and not TESTING: # pragma: no cover 1178 ↛ 1182line 1178 didn't jump to line 1182 because the condition on line 1178 was always true
1179 SPECTACULAR_SETTINGS['SERVERS'] = [{'url': SITE_URL}]
1181# Storage backends
1182STORAGE_TARGET, STORAGES, _media = storages.init_storages()
1183if 'dbbackup' not in STORAGES: 1183 ↛ 1185line 1183 didn't jump to line 1185 because the condition on line 1183 was always true
1184 STORAGES['dbbackup'] = DBBACKUP_STORAGE_CONFIG
1185if _media: 1185 ↛ 1186line 1185 didn't jump to line 1186 because the condition on line 1185 was never true
1186 MEDIA_URL = _media
1187PRESIGNED_URL_EXPIRATION = 600
1189# Taggit settings
1190TAGGIT_CASE_INSENSITIVE = True