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

1"""Django settings for InvenTree project. 

2 

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. 

6 

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""" 

11 

12import logging 

13import os 

14import sys 

15from typing import Optional 

16from zoneinfo import ZoneInfo, ZoneInfoNotFoundError 

17 

18import django.conf.locale 

19import django.core.exceptions 

20from django.core.validators import URLValidator 

21from django.http import Http404, HttpResponseGone 

22 

23import structlog 

24from corsheaders.defaults import default_headers as default_cors_headers 

25 

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 

33 

34from . import config 

35from .setting import ( 

36 db_backend, 

37 ldap, 

38 locales, 

39 markdown, 

40 spectacular, 

41 storages, 

42 tracing, 

43 worker, 

44) 

45 

46try: 

47 import django_stubs_ext 

48 

49 django_stubs_ext.monkeypatch() # pragma: no cover 

50except ImportError: # pragma: no cover 

51 pass 

52 

53checkMinPythonVersion() 

54 

55INVENTREE_BASE_URL = 'https://inventree.org' 

56INVENTREE_NEWS_URL = f'{INVENTREE_BASE_URL}/news/feed.atom' 

57 

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) 

64 

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'] 

68 

69 # Enable slow-test-runner 

70 TEST_RUNNER = 'django_slowtests.testrunner.DiscoverSlowestTestsRunner' 

71 NUM_SLOW_TESTS = 25 

72 

73 

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 

78 

79# New requirement for django 3.2+ 

80DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 

81 

82# Build paths inside the project like this: BASE_DIR.joinpath(...) 

83BASE_DIR = config.get_base_dir() 

84 

85# Load configuration data 

86CONFIG = config.load_config_data(set_cache=True) 

87config.load_version_file() 

88 

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) 

92 

93# Internal flag to determine if we are running in docker mode 

94DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False) 

95 

96AUTO_UPDATE = get_boolean_setting('INVENTREE_AUTO_UPDATE', 'auto_update', False) 

97 

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) 

103 

104logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s %(levelname)s %(message)s') 

105 

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} 

143 

144 

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') 

160 

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'}} 

180 

181# Get a logger instance for this setup file 

182logger = structlog.getLogger('inventree') 

183 

184# Load SECRET_KEY 

185SECRET_KEY = config.get_secret_key() 

186 

187# The filesystem location for served static files 

188STATIC_ROOT = config.get_static_dir() 

189 

190# The filesystem location for uploaded media files 

191MEDIA_ROOT = config.get_media_dir() 

192 

193# Needed for the parts importer, directly impacts the maximum parts that can be uploaded 

194DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 

195 

196# Web URL endpoint for served static files 

197STATIC_URL = '/static/' 

198 

199# Web URL endpoint for served media files 

200MEDIA_URL = '/media/' 

201 

202# Are plugins enabled? 

203PLUGINS_ENABLED = get_boolean_setting( 

204 'INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False 

205) 

206 

207PLUGINS_MANDATORY = get_setting( 

208 'INVENTREE_PLUGINS_MANDATORY', 'plugins_mandatory', typecast=list, default_value=[] 

209) 

210 

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) 

213 

214PLUGINS_INSTALL_DISABLED = get_boolean_setting( 

215 'INVENTREE_PLUGIN_NOINSTALL', 'plugin_noinstall', False 

216) 

217 

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 ) 

222 

223PLUGIN_FILE = config.get_plugin_file() 

224 

225# Plugin test settings 

226PLUGIN_TESTING = get_setting( 

227 'INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING 

228) # Are plugins being tested? 

229 

230PLUGIN_TESTING_SETUP = get_setting( 

231 'INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False 

232) # Load plugins from setup hooks in testing? 

233 

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) 

237 

238# Plugin development settings 

239PLUGIN_DEV_SLUG = get_setting('INVENTREE_PLUGIN_DEV_SLUG', 'plugin_dev.slug') 

240 

241PLUGIN_DEV_HOST = get_setting( 

242 'INVENTREE_PLUGIN_DEV_HOST', 'plugin_dev.host', 'http://localhost:5174' 

243) # Host for the plugin development server 

244 

245PLUGIN_RETRY = get_setting( 

246 'INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 3, typecast=int 

247) # How often should plugin loading be tried? 

248 

249# Hash of the plugin file (will be updated on each change) 

250PLUGIN_FILE_HASH = '' 

251 

252STATICFILES_DIRS = [] 

253 

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) 

259 

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')) 

264 

265# Database backup options 

266# Ref: https://archmonger.github.io/django-dbbackup/latest/configuration/ 

267 

268# For core backup functionality, refer to the STORAGES["dbbackup"] entry (below) 

269 

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() 

273 

274DBBACKUP_GPG_RECIPIENT = InvenTree.backup.backup_gpg_recipient() 

275 

276DBBACKUP_SEND_EMAIL = InvenTree.backup.backup_email_on_error() 

277DBBACKUP_EMAIL_SUBJECT_PREFIX = InvenTree.backup.backup_email_prefix() 

278 

279DBBACKUP_CONNECTORS = {'default': InvenTree.backup.get_backup_connector_options()} 

280 

281DBBACKUP_BACKUP_METADATA_SETTER = InvenTree.backup.metadata_set 

282DBBACKUP_RESTORE_METADATA_VALIDATOR = InvenTree.backup.validate_restore 

283 

284# Data storage options 

285DBBACKUP_STORAGE_CONFIG = { 

286 'BACKEND': InvenTree.backup.get_backup_storage_backend(), 

287 'OPTIONS': InvenTree.backup.get_backup_storage_options(), 

288} 

289 

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) 

296 

297# Enable django admin interface? 

298INVENTREE_ADMIN_ENABLED = get_boolean_setting( 

299 'INVENTREE_ADMIN_ENABLED', config_key='admin_enabled', default_value=True 

300) 

301 

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) 

306 

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] 

369 

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) 

398 

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) 

407 

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') 

411 

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 ) 

416 

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') 

427 

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} 

440 

441 

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] 

449 

450AUTHENTICATION_BACKENDS = ( 

451 CONFIG.get('authentication_backends', default_auth_backends) 

452 if CONFIG 

453 else default_auth_backends 

454) 

455 

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) 

461 

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') 

464 

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 } 

473 

474 # Determine LDAP settings 

475 ldap_settings = ldap.get_ldap_config(debug=LDAP_DEBUG) 

476 globals().update(ldap_settings) 

477 

478 

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') 

482 

483# InvenTree URL configuration 

484ROOT_URLCONF = 'InvenTree.urls' 

485 

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] 

511 

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} 

533 

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 ) 

539 

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') 

546 

547# WSGI default setting 

548WSGI_APPLICATION = 'InvenTree.wsgi.application' 

549 

550""" 

551Configure the database backend based on the user-specified values. 

552 

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""" 

557 

558logger.debug('Configuring database backend:') 

559 

560# Load database configuration from the config file and environment variables 

561database = db_backend.get_db_backend() 

562DB_ENGINE = database['ENGINE'] 

563 

564DATABASES = {'default': database} 

565 

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) 

573 

574# region Tracing / error tracking 

575inventree_tags = { 

576 'testing': TESTING, 

577 'docker': DOCKER, 

578 'debug': DEBUG, 

579 'remote': REMOTE_LOGIN, 

580 'commit': inventreeCommitHash(), 

581} 

582 

583# sentry.io integration for error reporting 

584SENTRY_ENABLED = not TESTING and get_boolean_setting( 

585 'INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False 

586) 

587 

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) 

593 

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) 

596 

597# OpenTelemetry tracing 

598TRACING_ENABLED = ( 

599 get_boolean_setting('INVENTREE_TRACING_ENABLED', 'tracing.enabled', False) 

600 and not isRunningBackup() 

601) 

602 

603TRACING_DETAILS: Optional[dict] = tracing.configure_tracing( 

604 DB_ENGINE, TRACING_ENABLED, inventree_tags 

605) 

606 

607# Cache configuration 

608GLOBAL_CACHE_ENABLED = is_global_cache_enabled() 

609 

610CACHES = {'default': get_cache_config(GLOBAL_CACHE_ENABLED)} 

611 

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) 

619 

620SILENCED_SYSTEM_CHECKS = ['templates.E003', 'templates.W003'] 

621 

622# Password validation 

623# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 

624 

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] 

633 

634# Extra (optional) URL validators 

635# See https://docs.djangoproject.com/en/2.2/ref/validators/#django.core.validators.URLValidator 

636 

637EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', []) 

638 

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 = [] 

642 

643LANGUAGES = locales.LOCALES 

644 

645LOCALE_CODES = [lang[0] for lang in LANGUAGES] 

646 

647# Internationalization 

648# https://docs.djangoproject.com/en/dev/topics/i18n/ 

649LANGUAGE_CODE = get_setting('INVENTREE_LANGUAGE', 'language', 'en-us') 

650 

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' 

659 

660# Store language settings for 30 days 

661LANGUAGE_COOKIE_AGE = 2592000 

662 

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' 

667 

668 # Add to language catalog 

669 LANGUAGES.append(('xx', 'Test')) 

670 

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 

675 

676# Maximum number of decimal places for currency rendering 

677CURRENCY_DECIMAL_PLACES = 6 

678 

679# Custom currency exchange backend 

680EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange' 

681 

682# region email 

683# Email configuration options 

684EMAIL_BACKEND = 'InvenTree.backends.InvenTreeMailLoggingBackend' 

685 

686INTERNAL_EMAIL_BACKEND = get_setting( 

687 'INVENTREE_EMAIL_BACKEND', 

688 'email.backend', 

689 'django.core.mail.backends.smtp.EmailBackend', 

690) 

691 

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) 

702 

703EMAIL_SUBJECT_PREFIX = get_setting( 

704 'INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ' 

705) 

706DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '') 

707 

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', '') 

711 

712EMAIL_USE_LOCALTIME = False 

713EMAIL_TIMEOUT = 60 

714# endregion email 

715 

716LOCALE_PATHS = (BASE_DIR.joinpath('locale/'),) 

717 

718TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC') 

719 

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") 

725 

726USE_I18N = True 

727 

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) 

731 

732DATE_INPUT_FORMATS = ['%Y-%m-%d'] 

733 

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) 

739 

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) 

743 

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) 

753 

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 ) 

759 

760# Enable or disable multi-site framework 

761SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False) 

762 

763# If a SITE_ID is specified 

764SITE_ID = get_setting('INVENTREE_SITE_ID', 'site_id', 1 if SITE_MULTI else None) 

765 

766# Load the allauth social backends 

767SOCIAL_BACKENDS = get_setting( 

768 'INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list 

769) 

770 

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') 

773 

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) 

782 

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) 

785 

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 ) 

796 

797 # Server cannot run without ALLOWED_HOSTS 

798 if isInMainThread(): 

799 sys.exit(-1) 

800 

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] 

805 

806 if ':' in host: 

807 ALLOWED_HOSTS[i] = host = host.split(':')[0] 

808 

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) 

817 

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) 

821 

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) 

832 

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) 

841 

842COOKIE_MODE = ( 

843 str(get_setting('INVENTREE_COOKIE_SAMESITE', 'cookie.samesite', 'False')) 

844 .lower() 

845 .strip() 

846) 

847 

848# Valid modes (as per the django settings documentation) 

849valid_cookie_modes = ['lax', 'strict', 'none'] 

850 

851COOKIE_MODE = COOKIE_MODE.capitalize() if COOKIE_MODE in valid_cookie_modes else False 

852 

853# Additional CSRF settings 

854CSRF_FAILURE_VIEW = 'InvenTree.middleware.csrf_failure' 

855CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' 

856CSRF_COOKIE_NAME = 'csrftoken' 

857 

858CSRF_COOKIE_SAMESITE = COOKIE_MODE 

859SESSION_COOKIE_SAMESITE = COOKIE_MODE 

860LANGUAGE_COOKIE_SAMESITE = COOKIE_MODE 

861 

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) 

871 

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 

875 

876CSRF_COOKIE_SECURE = COOKIE_SECURE 

877SESSION_COOKIE_SECURE = COOKIE_SECURE 

878LANGUAGE_COOKIE_SECURE = COOKIE_SECURE 

879 

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') 

891 

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) 

897 

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) 

903 

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 

907 

908 

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) 

913 

914CORS_ALLOW_CREDENTIALS = get_boolean_setting( 

915 'INVENTREE_CORS_ALLOW_CREDENTIALS', 

916 config_key='cors.allow_credentials', 

917 default_value=True, 

918) 

919 

920# Only allow CORS access to the following URL endpoints 

921CORS_URLS_REGEX = r'^/(api|auth|media|plugin|static)/.*$' 

922 

923CORS_ALLOWED_ORIGINS = get_setting( 

924 'INVENTREE_CORS_ORIGIN_WHITELIST', 

925 config_key='cors.whitelist', 

926 default_value=[], 

927 typecast=list, 

928) 

929 

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) 

933 

934CORS_ALLOWED_ORIGIN_REGEXES = get_setting( 

935 'INVENTREE_CORS_ORIGIN_REGEX', 

936 config_key='cors.regex', 

937 default_value=[], 

938 typecast=list, 

939) 

940 

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 

947 

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+$') 

952 

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) 

958 

959 if CORS_ALLOWED_ORIGIN_REGEXES: 

960 logger.info('CORS: Whitelisted origin regexes: %s', CORS_ALLOWED_ORIGIN_REGEXES) 

961 

962# Load settings for the frontend interface 

963FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG) 

964FRONTEND_URL_BASE = FRONTEND_SETTINGS['base_url'] 

965 

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.' 

970 

971 if not app.startswith(social_prefix): 

972 app = social_prefix + app 

973 

974 INSTALLED_APPS.append(app) 

975 

976SOCIALACCOUNT_PROVIDERS = get_setting( 

977 'INVENTREE_SOCIAL_PROVIDERS', 'social_providers', None, typecast=dict 

978) 

979 

980SOCIALACCOUNT_STORE_TOKENS = True 

981 

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 = '' 

986 

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 

994 

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) 

998 

999try: 

1000 login_attempts = int(login_attempts) 

1001 login_attempts = f'{login_attempts}/m,{login_attempts}/m' 

1002except ValueError: # pragma: no cover 

1003 pass 

1004 

1005ACCOUNT_RATE_LIMITS = {'login_failed': login_attempts} 

1006 

1007# Default protocol for login 

1008ACCOUNT_DEFAULT_HTTP_PROTOCOL = get_setting( 

1009 'INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL', 'login_default_protocol', None 

1010) 

1011 

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' 

1019 

1020ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True 

1021ACCOUNT_PREVENT_ENUMERATION = True 

1022ACCOUNT_EMAIL_SUBJECT_PREFIX = EMAIL_SUBJECT_PREFIX 

1023# 2FA 

1024REMOVE_SUCCESS_URL = 'settings' 

1025 

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} 

1037 

1038SOCIALACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomSocialAccountAdapter' 

1039ACCOUNT_ADAPTER = 'InvenTree.auth_overrides.CustomAccountAdapter' 

1040HEADLESS_ADAPTER = 'InvenTree.auth_overrides.CustomHeadlessAdapter' 

1041ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True 

1042 

1043HEADLESS_ONLY = True 

1044HEADLESS_CLIENTS = 'browser' 

1045MFA_ENABLED = get_boolean_setting('INVENTREE_MFA_ENABLED', 'mfa_enabled', True) 

1046 

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') 

1049 

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) 

1060 

1061MFA_TRUST_ENABLED = True 

1062MFA_PASSKEY_LOGIN_ENABLED = True 

1063MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = DEBUG 

1064 

1065LOGOUT_REDIRECT_URL = get_setting( 

1066 'INVENTREE_LOGOUT_REDIRECT_URL', 'logout_redirect_url', 'index' 

1067) 

1068# endregion auth 

1069 

1070# Markdownify configuration 

1071# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html 

1072 

1073MARKDOWNIFY = markdown.markdownify_config() 

1074 

1075# Ignore these error types for in-database error logging 

1076IGNORED_ERRORS = [Http404, HttpResponseGone, django.core.exceptions.PermissionDenied] 

1077 

1078# Maintenance mode 

1079MAINTENANCE_MODE_RETRY_AFTER = 10 

1080MAINTENANCE_MODE_STATE_BACKEND = 'InvenTree.backends.InvenTreeMaintenanceModeBackend' 

1081 

1082# Flag to allow table events during testing 

1083TESTING_TABLE_EVENTS = False 

1084 

1085# Flag to allow pricing recalculations during testing 

1086TESTING_PRICING = False 

1087 

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) 

1093 

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 

1097 

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) 

1103 

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) 

1110 

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) 

1118 

1119# User interface customization values 

1120CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', typecast=str) 

1121 

1122CUSTOM_SPLASH = get_setting('INVENTREE_CUSTOM_SPLASH', 'customize.splash', typecast=str) 

1123 

1124CUSTOMIZE = get_setting( 

1125 'INVENTREE_CUSTOMIZE', 'customize', default_value=None, typecast=dict 

1126) 

1127 

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') 

1130 

1131logger.info("MEDIA_ROOT: '%s'", MEDIA_ROOT) 

1132logger.info("STATIC_ROOT: '%s'", STATIC_ROOT) 

1133 

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} 

1145 

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) 

1154 

1155# Magic login django-sesame 

1156SESAME_MAX_AGE = 300 

1157LOGIN_REDIRECT_URL = '/api/auth/login-redirect/' 

1158 

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) 

1164 

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] 

1177 

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}] 

1180 

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 

1188 

1189# Taggit settings 

1190TAGGIT_CASE_INSENSITIVE = True