• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / InvenTree / 4310093436

pending completion
4310093436

push

github

GitHub
Slow tests (#4435)

17 of 17 new or added lines in 2 files covered. (100.0%)

25317 of 28870 relevant lines covered (87.69%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.16
/InvenTree/InvenTree/settings.py
1
"""Django settings for InvenTree project.
2

3
In practice the settings in this file should not be adjusted,
4
instead settings can be configured in the config.yaml file
5
located in the top level project directory.
6

7
This allows implementation configuration to be hidden from source control,
8
as well as separate configuration parameters from the more complex
9
database setup in this file.
10
"""
11

12
import logging
1✔
13
import os
1✔
14
import socket
1✔
15
import sys
1✔
16
from pathlib import Path
1✔
17

18
import django.conf.locale
1✔
19
import django.core.exceptions
1✔
20
from django.http import Http404
1✔
21
from django.utils.translation import gettext_lazy as _
1✔
22

23
import moneyed
1✔
24
import sentry_sdk
1✔
25
from sentry_sdk.integrations.django import DjangoIntegration
1✔
26

27
from . import config
1✔
28
from .config import get_boolean_setting, get_custom_file, get_setting
1✔
29

30
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
1✔
31

32
# Determine if we are running in "test" mode e.g. "manage.py test"
33
TESTING = 'test' in sys.argv
1✔
34

35
if TESTING:
1✔
36

37
    # Use a weaker password hasher for testing (improves testing speed)
38
    PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher',]
1✔
39

40
    # Enable slow-test-runner
41
    TEST_RUNNER = 'django_slowtests.testrunner.DiscoverSlowestTestsRunner'
1✔
42
    NUM_SLOW_TESTS = 25
1✔
43

44
    # Note: The following fix is "required" for docker build workflow
45
    # Note: 2022-12-12 still unsure why...
46
    if os.getenv('INVENTREE_DOCKER'):
1✔
47
        # Ensure that sys.path includes global python libs
48
        site_packages = '/usr/local/lib/python3.9/site-packages'
×
49

50
        if site_packages not in sys.path:
×
51
            print("Adding missing site-packages path:", site_packages)
×
52
            sys.path.append(site_packages)
×
53

54
# Are environment variables manipulated by tests? Needs to be set by testing code
55
TESTING_ENV = False
1✔
56

57
# New requirement for django 3.2+
58
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
1✔
59

60
# Build paths inside the project like this: BASE_DIR.joinpath(...)
61
BASE_DIR = config.get_base_dir()
1✔
62

63
# Load configuration data
64
CONFIG = config.load_config_data(set_cache=True)
1✔
65

66
# Default action is to run the system in Debug mode
67
# SECURITY WARNING: don't run with debug turned on in production!
68
DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', True)
1✔
69

70
# Configure logging settings
71
log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
1✔
72

73
logging.basicConfig(
1✔
74
    level=log_level,
75
    format="%(asctime)s %(levelname)s %(message)s",
76
)
77

78
if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
1✔
79
    log_level = 'WARNING'  # pragma: no cover
80

81
LOGGING = {
1✔
82
    'version': 1,
83
    'disable_existing_loggers': False,
84
    'handlers': {
85
        'console': {
86
            'class': 'logging.StreamHandler',
87
        },
88
    },
89
    'root': {
90
        'handlers': ['console'],
91
        'level': log_level,
92
    },
93
    'filters': {
94
        'require_not_maintenance_mode_503': {
95
            '()': 'maintenance_mode.logging.RequireNotMaintenanceMode503',
96
        },
97
    },
98
}
99

100
# Get a logger instance for this setup file
101
logger = logging.getLogger("inventree")
1✔
102

103
# Load SECRET_KEY
104
SECRET_KEY = config.get_secret_key()
1✔
105

106
# The filesystem location for served static files
107
STATIC_ROOT = config.get_static_dir()
1✔
108

109
# The filesystem location for uploaded meadia files
110
MEDIA_ROOT = config.get_media_dir()
1✔
111

112
# List of allowed hosts (default = allow all)
113
ALLOWED_HOSTS = get_setting(
1✔
114
    "INVENTREE_ALLOWED_HOSTS",
115
    config_key='allowed_hosts',
116
    default_value=['*'],
117
    typecast=list,
118
)
119

120
# Cross Origin Resource Sharing (CORS) options
121

122
# Only allow CORS access to API
123
CORS_URLS_REGEX = r'^/api/.*$'
1✔
124

125
# Extract CORS options from configuration file
126
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
1✔
127
    "INVENTREE_CORS_ORIGIN_ALLOW_ALL",
128
    config_key='cors.allow_all',
129
    default_value=False,
130
)
131

132
CORS_ORIGIN_WHITELIST = get_setting(
1✔
133
    "INVENTREE_CORS_ORIGIN_WHITELIST",
134
    config_key='cors.whitelist',
135
    default_value=[],
136
    typecast=list,
137
)
138

139
# Needed for the parts importer, directly impacts the maximum parts that can be uploaded
140
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
1✔
141

142
# Web URL endpoint for served static files
143
STATIC_URL = '/static/'
1✔
144

145
STATICFILES_DIRS = []
1✔
146

147
# Translated Template settings
148
STATICFILES_I18_PREFIX = 'i18n'
1✔
149
STATICFILES_I18_SRC = BASE_DIR.joinpath('templates', 'js', 'translated')
1✔
150
STATICFILES_I18_TRG = BASE_DIR.joinpath('InvenTree', 'static_i18n')
1✔
151
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
1✔
152
STATICFILES_I18_TRG = STATICFILES_I18_TRG.joinpath(STATICFILES_I18_PREFIX)
1✔
153

154
STATFILES_I18_PROCESSORS = [
1✔
155
    'InvenTree.context.status_codes',
156
]
157

158
# Color Themes Directory
159
STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes').resolve()
1✔
160

161
# Web URL endpoint for served media files
162
MEDIA_URL = '/media/'
1✔
163

164
# Database backup options
165
# Ref: https://django-dbbackup.readthedocs.io/en/master/configuration.html
166
DBBACKUP_SEND_EMAIL = False
1✔
167
DBBACKUP_STORAGE = get_setting(
1✔
168
    'INVENTREE_BACKUP_STORAGE',
169
    'backup_storage',
170
    'django.core.files.storage.FileSystemStorage'
171
)
172

173
# Default backup configuration
174
DBBACKUP_STORAGE_OPTIONS = get_setting('INVENTREE_BACKUP_OPTIONS', 'backup_options', None)
1✔
175
if DBBACKUP_STORAGE_OPTIONS is None:
1✔
176
    DBBACKUP_STORAGE_OPTIONS = {
1✔
177
        'location': config.get_backup_dir(),
178
    }
179

180
# Application definition
181

182
INSTALLED_APPS = [
1✔
183
    # Admin site integration
184
    'django.contrib.admin',
185

186
    # InvenTree apps
187
    'build.apps.BuildConfig',
188
    'common.apps.CommonConfig',
189
    'company.apps.CompanyConfig',
190
    'label.apps.LabelConfig',
191
    'order.apps.OrderConfig',
192
    'part.apps.PartConfig',
193
    'report.apps.ReportConfig',
194
    'stock.apps.StockConfig',
195
    'users.apps.UsersConfig',
196
    'plugin.apps.PluginAppConfig',
197
    'InvenTree.apps.InvenTreeConfig',       # InvenTree app runs last
198

199
    # Core django modules
200
    'django.contrib.auth',
201
    'django.contrib.contenttypes',
202
    'user_sessions',                # db user sessions
203
    'django.contrib.messages',
204
    'django.contrib.staticfiles',
205
    'django.contrib.sites',
206

207
    # Maintenance
208
    'maintenance_mode',
209

210
    # Third part add-ons
211
    'django_filters',                       # Extended filter functionality
212
    'rest_framework',                       # DRF (Django Rest Framework)
213
    'rest_framework.authtoken',             # Token authentication for API
214
    'corsheaders',                          # Cross-origin Resource Sharing for DRF
215
    'crispy_forms',                         # Improved form rendering
216
    'import_export',                        # Import / export tables to file
217
    'django_cleanup.apps.CleanupConfig',    # Automatically delete orphaned MEDIA files
218
    'mptt',                                 # Modified Preorder Tree Traversal
219
    'markdownify',                          # Markdown template rendering
220
    'djmoney',                              # django-money integration
221
    'djmoney.contrib.exchange',             # django-money exchange rates
222
    'error_report',                         # Error reporting in the admin interface
223
    'django_q',
224
    'formtools',                            # Form wizard tools
225
    'dbbackup',                             # Backups - django-dbbackup
226

227
    'allauth',                              # Base app for SSO
228
    'allauth.account',                      # Extend user with accounts
229
    'allauth.socialaccount',                # Use 'social' providers
230

231
    'django_otp',                           # OTP is needed for MFA - base package
232
    'django_otp.plugins.otp_totp',          # Time based OTP
233
    'django_otp.plugins.otp_static',        # Backup codes
234

235
    'allauth_2fa',                          # MFA flow for allauth
236

237
    'django_ical',                          # For exporting calendars
238
]
239

240
MIDDLEWARE = CONFIG.get('middleware', [
1✔
241
    'django.middleware.security.SecurityMiddleware',
242
    'x_forwarded_for.middleware.XForwardedForMiddleware',
243
    'user_sessions.middleware.SessionMiddleware',                   # db user sessions
244
    'django.middleware.locale.LocaleMiddleware',
245
    'django.middleware.common.CommonMiddleware',
246
    'django.middleware.csrf.CsrfViewMiddleware',
247
    'corsheaders.middleware.CorsMiddleware',
248
    'django.contrib.auth.middleware.AuthenticationMiddleware',
249
    'InvenTree.middleware.InvenTreeRemoteUserMiddleware',       # Remote / proxy auth
250
    'django_otp.middleware.OTPMiddleware',                      # MFA support
251
    'InvenTree.middleware.CustomAllauthTwoFactorMiddleware',    # Flow control for allauth
252
    'django.contrib.messages.middleware.MessageMiddleware',
253
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
254
    'InvenTree.middleware.AuthRequiredMiddleware',
255
    'InvenTree.middleware.Check2FAMiddleware',                  # Check if the user should be forced to use MFA
256
    'maintenance_mode.middleware.MaintenanceModeMiddleware',
257
    'InvenTree.middleware.InvenTreeExceptionProcessor',         # Error reporting
258
])
259

260
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
1✔
261
    'django.contrib.auth.backends.RemoteUserBackend',           # proxy login
262
    'django.contrib.auth.backends.ModelBackend',
263
    'allauth.account.auth_backends.AuthenticationBackend',      # SSO login via external providers
264
])
265

266
DEBUG_TOOLBAR_ENABLED = DEBUG and get_setting('INVENTREE_DEBUG_TOOLBAR', 'debug_toolbar', False)
1✔
267

268
# If the debug toolbar is enabled, add the modules
269
if DEBUG_TOOLBAR_ENABLED:  # pragma: no cover
270
    logger.info("Running with DEBUG_TOOLBAR enabled")
271
    INSTALLED_APPS.append('debug_toolbar')
272
    MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
273

274
    DEBUG_TOOLBAR_CONFIG = {
275
        'RESULTS_CACHE_SIZE': 100,
276
        'OBSERVE_REQUEST_CALLBACK': lambda x: False,
277
    }
278

279
# Internal IP addresses allowed to see the debug toolbar
280
INTERNAL_IPS = [
1✔
281
    '127.0.0.1',
282
]
283

284
# Internal flag to determine if we are running in docker mode
285
DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False)
1✔
286

287
if DOCKER:  # pragma: no cover
288
    # Internal IP addresses are different when running under docker
289
    hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname())
290
    INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"]
291

292
# Allow secure http developer server in debug mode
293
if DEBUG:
1✔
294
    INSTALLED_APPS.append('sslserver')
1✔
295

296
# InvenTree URL configuration
297

298
# Base URL for admin pages (default="admin")
299
INVENTREE_ADMIN_URL = get_setting(
1✔
300
    'INVENTREE_ADMIN_URL',
301
    config_key='admin_url',
302
    default_value='admin'
303
)
304

305
ROOT_URLCONF = 'InvenTree.urls'
1✔
306

307
TEMPLATES = [
1✔
308
    {
309
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
310
        'DIRS': [
311
            BASE_DIR.joinpath('templates'),
312
            # Allow templates in the reporting directory to be accessed
313
            MEDIA_ROOT.joinpath('report'),
314
            MEDIA_ROOT.joinpath('label'),
315
        ],
316
        'OPTIONS': {
317
            'context_processors': [
318
                'django.template.context_processors.debug',
319
                'django.template.context_processors.request',
320
                'django.template.context_processors.i18n',
321
                'django.contrib.auth.context_processors.auth',
322
                'django.contrib.messages.context_processors.messages',
323
                # Custom InvenTree context processors
324
                'InvenTree.context.health_status',
325
                'InvenTree.context.status_codes',
326
                'InvenTree.context.user_roles',
327
            ],
328
            'loaders': [(
329
                'django.template.loaders.cached.Loader', [
330
                    'plugin.template.PluginTemplateLoader',
331
                    'django.template.loaders.filesystem.Loader',
332
                    'django.template.loaders.app_directories.Loader',
333
                ])
334
            ],
335
        },
336
    },
337
]
338

339
if DEBUG_TOOLBAR_ENABLED:  # pragma: no cover
340
    # Note that the APP_DIRS value must be set when using debug_toolbar
341
    # But this will kill template loading for plugins
342
    TEMPLATES[0]['APP_DIRS'] = True
343
    del TEMPLATES[0]['OPTIONS']['loaders']
344

345
REST_FRAMEWORK = {
1✔
346
    'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
347
    'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
348
    'DEFAULT_AUTHENTICATION_CLASSES': (
349
        'rest_framework.authentication.BasicAuthentication',
350
        'rest_framework.authentication.SessionAuthentication',
351
        'rest_framework.authentication.TokenAuthentication',
352
    ),
353
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
354
    'DEFAULT_PERMISSION_CLASSES': (
355
        'rest_framework.permissions.IsAuthenticated',
356
        'rest_framework.permissions.DjangoModelPermissions',
357
        'InvenTree.permissions.RolePermission',
358
    ),
359
    'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
360
    'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
361
    'DEFAULT_RENDERER_CLASSES': [
362
        'rest_framework.renderers.JSONRenderer',
363
    ]
364
}
365

366
if DEBUG:
1✔
367
    # Enable browsable API if in DEBUG mode
368
    REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('rest_framework.renderers.BrowsableAPIRenderer')
1✔
369

370
WSGI_APPLICATION = 'InvenTree.wsgi.application'
1✔
371

372
"""
373
Configure the database backend based on the user-specified values.
374

375
- Primarily this configuration happens in the config.yaml file
376
- However there may be reason to configure the DB via environmental variables
377
- The following code lets the user "mix and match" database configuration
378
"""
379

380
logger.debug("Configuring database backend:")
1✔
381

382
# Extract database configuration from the config.yaml file
383
db_config = CONFIG.get('database', {})
1✔
384

385
if not db_config:
1✔
386
    db_config = {}
1✔
387

388
# Environment variables take preference over config file!
389

390
db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
1✔
391

392
for key in db_keys:
1✔
393
    # First, check the environment variables
394
    env_key = f"INVENTREE_DB_{key}"
1✔
395
    env_var = os.environ.get(env_key, None)
1✔
396

397
    if env_var:
1✔
398
        # Make use PORT is int
399
        if key == 'PORT':
1✔
400
            try:
×
401
                env_var = int(env_var)
×
402
            except ValueError:
×
403
                logger.error(f"Invalid number for {env_key}: {env_var}")
×
404
        # Override configuration value
405
        db_config[key] = env_var
1✔
406

407
# Check that required database configuration options are specified
408
required_keys = ['ENGINE', 'NAME']
1✔
409

410
for key in required_keys:
1✔
411
    if key not in db_config:  # pragma: no cover
412
        error_msg = f'Missing required database configuration value {key}'
413
        logger.error(error_msg)
414

415
        print('Error: ' + error_msg)
416
        sys.exit(-1)
417

418
"""
419
Special considerations for the database 'ENGINE' setting.
420
It can be specified in config.yaml (or envvar) as either (for example):
421
- sqlite3
422
- django.db.backends.sqlite3
423
- django.db.backends.postgresql
424
"""
425

426
db_engine = db_config['ENGINE'].lower()
1✔
427

428
# Correct common misspelling
429
if db_engine == 'sqlite':
1✔
430
    db_engine = 'sqlite3'  # pragma: no cover
431

432
if db_engine in ['sqlite3', 'postgresql', 'mysql']:
1✔
433
    # Prepend the required python module string
434
    db_engine = f'django.db.backends.{db_engine}'
1✔
435
    db_config['ENGINE'] = db_engine
1✔
436

437
db_name = db_config['NAME']
1✔
438
db_host = db_config.get('HOST', "''")
1✔
439

440
if 'sqlite' in db_engine:
1✔
441
    db_name = str(Path(db_name).resolve())
1✔
442
    db_config['NAME'] = db_name
1✔
443

444
logger.info(f"DB_ENGINE: {db_engine}")
1✔
445
logger.info(f"DB_NAME: {db_name}")
1✔
446
logger.info(f"DB_HOST: {db_host}")
1✔
447

448
"""
449
In addition to base-level database configuration, we may wish to specify specific options to the database backend
450
Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
451
"""
452

453
# 'OPTIONS' or 'options' can be specified in config.yaml
454
# Set useful sensible timeouts for a transactional webserver to communicate
455
# with its database server, that is, if the webserver is having issues
456
# connecting to the database server (such as a replica failover) don't sit and
457
# wait for possibly an hour or more, just tell the client something went wrong
458
# and let the client retry when they want to.
459
db_options = db_config.get("OPTIONS", db_config.get("options", {}))
1✔
460

461
# Specific options for postgres backend
462
if "postgres" in db_engine:  # pragma: no cover
463
    from psycopg2.extensions import (ISOLATION_LEVEL_READ_COMMITTED,
464
                                     ISOLATION_LEVEL_SERIALIZABLE)
465

466
    # Connection timeout
467
    if "connect_timeout" not in db_options:
468
        # The DB server is in the same data center, it should not take very
469
        # long to connect to the database server
470
        # # seconds, 2 is minium allowed by libpq
471
        db_options["connect_timeout"] = int(
472
            get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
473
        )
474

475
    # Setup TCP keepalive
476
    # DB server is in the same DC, it should not become unresponsive for
477
    # very long. With the defaults below we wait 5 seconds for the network
478
    # issue to resolve itself.  It it that doesn't happen whatever happened
479
    # is probably fatal and no amount of waiting is going to fix it.
480
    # # 0 - TCP Keepalives disabled; 1 - enabled
481
    if "keepalives" not in db_options:
482
        db_options["keepalives"] = int(
483
            get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
484
        )
485

486
    # Seconds after connection is idle to send keep alive
487
    if "keepalives_idle" not in db_options:
488
        db_options["keepalives_idle"] = int(
489
            get_setting('INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1)
490
        )
491

492
    # Seconds after missing ACK to send another keep alive
493
    if "keepalives_interval" not in db_options:
494
        db_options["keepalives_interval"] = int(
495
            get_setting("INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", "database.tcp_keepalives_internal", "1")
496
        )
497

498
    # Number of missing ACKs before we close the connection
499
    if "keepalives_count" not in db_options:
500
        db_options["keepalives_count"] = int(
501
            get_setting("INVENTREE_DB_TCP_KEEPALIVES_COUNT", "database.tcp_keepalives_count", "5")
502
        )
503

504
    # # Milliseconds for how long pending data should remain unacked
505
    # by the remote server
506
    # TODO: Supported starting in PSQL 11
507
    # "tcp_user_timeout": int(os.getenv("PGTCP_USER_TIMEOUT", "1000"),
508

509
    # Postgres's default isolation level is Read Committed which is
510
    # normally fine, but most developers think the database server is
511
    # actually going to do Serializable type checks on the queries to
512
    # protect against simultaneous changes.
513
    # https://www.postgresql.org/docs/devel/transaction-iso.html
514
    # https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
515
    if "isolation_level" not in db_options:
516
        serializable = get_boolean_setting('INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False)
517
        db_options["isolation_level"] = ISOLATION_LEVEL_SERIALIZABLE if serializable else ISOLATION_LEVEL_READ_COMMITTED
518

519
# Specific options for MySql / MariaDB backend
520
elif "mysql" in db_engine:  # pragma: no cover
521
    # TODO TCP time outs and keepalives
522

523
    # MariaDB's default isolation level is Repeatable Read which is
524
    # normally fine, but most developers think the database server is
525
    # actually going to Serializable type checks on the queries to
526
    # protect against siumltaneous changes.
527
    # https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level
528
    # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
529
    if "isolation_level" not in db_options:
530
        serializable = get_boolean_setting('INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False)
531
        db_options["isolation_level"] = "serializable" if serializable else "read committed"
532

533
# Specific options for sqlite backend
534
elif "sqlite" in db_engine:
1✔
535
    # TODO: Verify timeouts are not an issue because no network is involved for SQLite
536

537
    # SQLite's default isolation level is Serializable due to SQLite's
538
    # single writer implementation.  Presumably as a result of this, it is
539
    # not possible to implement any lower isolation levels in SQLite.
540
    # https://www.sqlite.org/isolation.html
541
    pass
542

543
# Provide OPTIONS dict back to the database configuration dict
544
db_config['OPTIONS'] = db_options
1✔
545

546
# Set testing options for the database
547
db_config['TEST'] = {
1✔
548
    'CHARSET': 'utf8',
549
}
550

551
# Set collation option for mysql test database
552
if 'mysql' in db_engine:
1✔
553
    db_config['TEST']['COLLATION'] = 'utf8_general_ci'  # pragma: no cover
554

555
DATABASES = {
1✔
556
    'default': db_config
557
}
558

559
# login settings
560
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
1✔
561
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
1✔
562

563
# sentry.io integration for error reporting
564
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
1✔
565
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
566
INVENTREE_DSN = 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
1✔
567
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', INVENTREE_DSN)
1✔
568
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1))
1✔
569

570
if SENTRY_ENABLED and SENTRY_DSN:  # pragma: no cover
571
    sentry_sdk.init(
572
        dsn=SENTRY_DSN,
573
        integrations=[DjangoIntegration(), ],
574
        traces_sample_rate=1.0 if DEBUG else SENTRY_SAMPLE_RATE,
575
        send_default_pii=True
576
    )
577
    inventree_tags = {
578
        'testing': TESTING,
579
        'docker': DOCKER,
580
        'debug': DEBUG,
581
        'remote': REMOTE_LOGIN,
582
    }
583
    for key, val in inventree_tags.items():
584
        sentry_sdk.set_tag(f'inventree_{key}', val)
585

586
# Cache configuration
587
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None)
1✔
588
cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379', typecast=int)
1✔
589

590
if cache_host:  # pragma: no cover
591
    # We are going to rely upon a possibly non-localhost for our cache,
592
    # so don't wait too long for the cache as nothing in the cache should be
593
    # irreplacable.
594
    _cache_options = {
595
        "CLIENT_CLASS": "django_redis.client.DefaultClient",
596
        "SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
597
        "SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")),
598
        "CONNECTION_POOL_KWARGS": {
599
            "socket_keepalive": config.is_true(
600
                os.getenv("CACHE_TCP_KEEPALIVE", "1")
601
            ),
602
            "socket_keepalive_options": {
603
                socket.TCP_KEEPCNT: int(
604
                    os.getenv("CACHE_KEEPALIVES_COUNT", "5")
605
                ),
606
                socket.TCP_KEEPIDLE: int(
607
                    os.getenv("CACHE_KEEPALIVES_IDLE", "1")
608
                ),
609
                socket.TCP_KEEPINTVL: int(
610
                    os.getenv("CACHE_KEEPALIVES_INTERVAL", "1")
611
                ),
612
                socket.TCP_USER_TIMEOUT: int(
613
                    os.getenv("CACHE_TCP_USER_TIMEOUT", "1000")
614
                ),
615
            },
616
        },
617
    }
618
    CACHES = {
619
        "default": {
620
            "BACKEND": "django_redis.cache.RedisCache",
621
            "LOCATION": f"redis://{cache_host}:{cache_port}/0",
622
            "OPTIONS": _cache_options,
623
        },
624
    }
625
else:
626
    CACHES = {
1✔
627
        "default": {
628
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
629
        },
630
    }
631

632
_q_worker_timeout = int(get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90))
1✔
633

634
# django-q background worker configuration
635
Q_CLUSTER = {
1✔
636
    'name': 'InvenTree',
637
    'label': 'Background Tasks',
638
    'workers': int(get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4)),
639
    'timeout': _q_worker_timeout,
640
    'retry': min(120, _q_worker_timeout + 30),
641
    'max_attempts': int(get_setting('INVENTREE_BACKGROUND_MAX_ATTEMPTS', 'background.max_attempts', 5)),
642
    'queue_limit': 50,
643
    'catch_up': False,
644
    'bulk': 10,
645
    'orm': 'default',
646
    'cache': 'default',
647
    'sync': False,
648
}
649

650
# Configure django-q sentry integration
651
if SENTRY_ENABLED and SENTRY_DSN:
1✔
652
    Q_CLUSTER['error_reporter'] = {
×
653
        'sentry': {
654
            'dsn': SENTRY_DSN
655
        }
656
    }
657

658
if cache_host:  # pragma: no cover
659
    # If using external redis cache, make the cache the broker for Django Q
660
    # as well
661
    Q_CLUSTER["django_redis"] = "worker"
662

663
# database user sessions
664
SESSION_ENGINE = 'user_sessions.backends.db'
1✔
665
LOGOUT_REDIRECT_URL = get_setting('INVENTREE_LOGOUT_REDIRECT_URL', 'logout_redirect_url', 'index')
1✔
666
SILENCED_SYSTEM_CHECKS = [
1✔
667
    'admin.E410',
668
]
669

670
# Password validation
671
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
672

673
AUTH_PASSWORD_VALIDATORS = [
1✔
674
    {
675
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
676
    },
677
    {
678
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
679
    },
680
    {
681
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
682
    },
683
    {
684
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
685
    },
686
]
687

688
# Extra (optional) URL validators
689
# See https://docs.djangoproject.com/en/2.2/ref/validators/#django.core.validators.URLValidator
690

691
EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', [])
1✔
692

693
if type(EXTRA_URL_SCHEMES) not in [list]:  # pragma: no cover
694
    logger.warning("extra_url_schemes not correctly formatted")
695
    EXTRA_URL_SCHEMES = []
696

697
# Internationalization
698
# https://docs.djangoproject.com/en/dev/topics/i18n/
699
LANGUAGE_CODE = get_setting('INVENTREE_LANGUAGE', 'language', 'en-us')
1✔
700
# Store language settings for 30 days
701
LANGUAGE_COOKIE_AGE = 2592000
1✔
702

703
# If a new language translation is supported, it must be added here
704
LANGUAGES = [
1✔
705
    ('cs', _('Czech')),
706
    ('da', _('Danish')),
707
    ('de', _('German')),
708
    ('el', _('Greek')),
709
    ('en', _('English')),
710
    ('es', _('Spanish')),
711
    ('es-mx', _('Spanish (Mexican)')),
712
    ('fa', _('Farsi / Persian')),
713
    ('fr', _('French')),
714
    ('he', _('Hebrew')),
715
    ('hu', _('Hungarian')),
716
    ('it', _('Italian')),
717
    ('ja', _('Japanese')),
718
    ('ko', _('Korean')),
719
    ('nl', _('Dutch')),
720
    ('no', _('Norwegian')),
721
    ('pl', _('Polish')),
722
    ('pt', _('Portuguese')),
723
    ('pt-BR', _('Portuguese (Brazilian)')),
724
    ('ru', _('Russian')),
725
    ('sl', _('Slovenian')),
726
    ('sv', _('Swedish')),
727
    ('th', _('Thai')),
728
    ('tr', _('Turkish')),
729
    ('vi', _('Vietnamese')),
730
    ('zh-hans', _('Chinese')),
731
]
732

733
# Testing interface translations
734
if get_boolean_setting('TEST_TRANSLATIONS', default_value=False):  # pragma: no cover
735
    # Set default language
736
    LANGUAGE_CODE = 'xx'
737

738
    # Add to language catalog
739
    LANGUAGES.append(('xx', 'Test'))
740

741
    # Add custom languages not provided by Django
742
    EXTRA_LANG_INFO = {
743
        'xx': {
744
            'code': 'xx',
745
            'name': 'Test',
746
            'name_local': 'Test'
747
        },
748
    }
749
    LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
750
    django.conf.locale.LANG_INFO = LANG_INFO
751

752
# Currencies available for use
753
CURRENCIES = get_setting(
1✔
754
    'INVENTREE_CURRENCIES', 'currencies',
755
    ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'NZD', 'USD'],
756
    typecast=list,
757
)
758

759
# Maximum number of decimal places for currency rendering
760
CURRENCY_DECIMAL_PLACES = 6
1✔
761

762
# Check that each provided currency is supported
763
for currency in CURRENCIES:
1✔
764
    if currency not in moneyed.CURRENCIES:  # pragma: no cover
765
        logger.error(f"Currency code '{currency}' is not supported")
766
        sys.exit(1)
767

768
# Custom currency exchange backend
769
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
1✔
770

771
# Email configuration options
772
EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend')
1✔
773
EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host', '')
1✔
774
EMAIL_PORT = get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25, typecast=int)
1✔
775
EMAIL_HOST_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
1✔
776
EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '')
1✔
777
EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ')
1✔
778
EMAIL_USE_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
1✔
779
EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
1✔
780

781
DEFAULT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
1✔
782

783
EMAIL_USE_LOCALTIME = False
1✔
784
EMAIL_TIMEOUT = 60
1✔
785

786
LOCALE_PATHS = (
1✔
787
    BASE_DIR.joinpath('locale/'),
788
)
789

790
TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
1✔
791

792
USE_I18N = True
1✔
793

794
USE_L10N = True
1✔
795

796
# Do not use native timezone support in "test" mode
797
# It generates a *lot* of cruft in the logs
798
if not TESTING:
1✔
799
    USE_TZ = True  # pragma: no cover
800

801
DATE_INPUT_FORMATS = [
1✔
802
    "%Y-%m-%d",
803
]
804

805
# crispy forms use the bootstrap templates
806
CRISPY_TEMPLATE_PACK = 'bootstrap4'
1✔
807

808
# Use database transactions when importing / exporting data
809
IMPORT_EXPORT_USE_TRANSACTIONS = True
1✔
810

811
SITE_ID = 1
1✔
812

813
# Load the allauth social backends
814
SOCIAL_BACKENDS = get_setting('INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list)
1✔
815

816
for app in SOCIAL_BACKENDS:
1✔
817
    INSTALLED_APPS.append(app)  # pragma: no cover
818

819
SOCIALACCOUNT_PROVIDERS = get_setting('INVENTREE_SOCIAL_PROVIDERS', 'social_providers', None)
1✔
820

821
if SOCIALACCOUNT_PROVIDERS is None:
1✔
822
    SOCIALACCOUNT_PROVIDERS = {}
1✔
823

824
SOCIALACCOUNT_STORE_TOKENS = True
1✔
825

826
# settings for allauth
827
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3, typecast=int)
1✔
828
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5, typecast=int)
1✔
829
ACCOUNT_DEFAULT_HTTP_PROTOCOL = get_setting('INVENTREE_LOGIN_DEFAULT_HTTP_PROTOCOL', 'login_default_protocol', 'http')
1✔
830
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
1✔
831
ACCOUNT_PREVENT_ENUMERATION = True
1✔
832

833
# override forms / adapters
834
ACCOUNT_FORMS = {
1✔
835
    'login': 'allauth.account.forms.LoginForm',
836
    'signup': 'InvenTree.forms.CustomSignupForm',
837
    'add_email': 'allauth.account.forms.AddEmailForm',
838
    'change_password': 'allauth.account.forms.ChangePasswordForm',
839
    'set_password': 'allauth.account.forms.SetPasswordForm',
840
    'reset_password': 'allauth.account.forms.ResetPasswordForm',
841
    'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
842
    'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
843
}
844

845
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
1✔
846
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
1✔
847

848
# Markdownify configuration
849
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
850

851
MARKDOWNIFY = {
1✔
852
    'default': {
853
        'BLEACH': True,
854
        'WHITELIST_ATTRS': [
855
            'href',
856
            'src',
857
            'alt',
858
        ],
859
        'MARKDOWN_EXTENSIONS': [
860
            'markdown.extensions.extra'
861
        ],
862
        'WHITELIST_TAGS': [
863
            'a',
864
            'abbr',
865
            'b',
866
            'blockquote',
867
            'em',
868
            'h1', 'h2', 'h3',
869
            'i',
870
            'img',
871
            'li',
872
            'ol',
873
            'p',
874
            'strong',
875
            'ul',
876
            'table',
877
            'thead',
878
            'tbody',
879
            'th',
880
            'tr',
881
            'td'
882
        ],
883
    }
884
}
885

886
# Ignore these error typeps for in-database error logging
887
IGNORED_ERRORS = [
1✔
888
    Http404,
889
    django.core.exceptions.PermissionDenied,
890
]
891

892
# Maintenance mode
893
MAINTENANCE_MODE_RETRY_AFTER = 60
1✔
894
MAINTENANCE_MODE_STATE_BACKEND = 'maintenance_mode.backends.StaticStorageBackend'
1✔
895

896
# Are plugins enabled?
897
PLUGINS_ENABLED = get_boolean_setting('INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False)
1✔
898

899
PLUGIN_FILE = config.get_plugin_file()
1✔
900

901
# Plugin test settings
902
PLUGIN_TESTING = get_setting('INVENTREE_PLUGIN_TESTING', 'PLUGIN_TESTING', TESTING)                     # Are plugins beeing tested?
1✔
903
PLUGIN_TESTING_SETUP = get_setting('INVENTREE_PLUGIN_TESTING_SETUP', 'PLUGIN_TESTING_SETUP', False)     # Load plugins from setup hooks in testing?
1✔
904
PLUGIN_TESTING_EVENTS = False                                                                           # Flag if events are tested right now
1✔
905
PLUGIN_RETRY = get_setting('INVENTREE_PLUGIN_RETRY', 'PLUGIN_RETRY', 5)                                 # How often should plugin loading be tried?
1✔
906
PLUGIN_FILE_CHECKED = False                                                                             # Was the plugin file checked?
1✔
907

908
# User interface customization values
909
CUSTOM_LOGO = get_custom_file('INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True)
1✔
910
CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', 'custom splash')
1✔
911

912
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
1✔
913
if DEBUG:
1✔
914
    logger.info("InvenTree running with DEBUG enabled")
1✔
915

916
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
1✔
917
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc