2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 03:55:41 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2022-08-01 13:32:54 +10:00
42 changed files with 6063 additions and 6074 deletions

3
.gitignore vendored
View File

@ -50,7 +50,6 @@ docs/_build
inventree_media
inventree_static
static_i18n
inventree-data
# Local config file
config.yaml
@ -79,6 +78,8 @@ js_tmp/
# Development files
dev/
data/
env/
# Locale stats file
locale_stats.json

View File

@ -18,6 +18,7 @@ pip install invoke && invoke setup-dev --tests
```bash
git clone https://github.com/inventree/InvenTree.git && cd InvenTree
docker compose run inventree-dev-server invoke install
docker compose run inventree-dev-server invoke setup-test
docker compose up -d
```

View File

@ -172,21 +172,13 @@ class InvenTreeConfig(AppConfig):
return
# get values
add_user = get_setting(
'INVENTREE_ADMIN_USER',
settings.CONFIG.get('admin_user', False)
)
add_email = get_setting(
'INVENTREE_ADMIN_EMAIL',
settings.CONFIG.get('admin_email', False)
)
add_password = get_setting(
'INVENTREE_ADMIN_PASSWORD',
settings.CONFIG.get('admin_password', False)
)
add_user = get_setting('INVENTREE_ADMIN_USER', 'admin_user')
add_email = get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email')
add_password = get_setting('INVENTREE_ADMIN_PASSWORD', 'admin_password')
# check if all values are present
set_variables = 0
for tested_var in [add_user, add_email, add_password]:
if tested_var:
set_variables += 1

View File

@ -2,18 +2,27 @@
import logging
import os
import random
import shutil
import string
from pathlib import Path
import yaml
logger = logging.getLogger('inventree')
def is_true(x):
"""Shortcut function to determine if a value "looks" like a boolean"""
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true', 'on']
def get_base_dir() -> Path:
"""Returns the base (top-level) InvenTree directory."""
return Path(__file__).parent.parent.resolve()
def get_config_file() -> Path:
def get_config_file(create=True) -> Path:
"""Returns the path of the InvenTree configuration file.
Note: It will be created it if does not already exist!
@ -28,7 +37,7 @@ def get_config_file() -> Path:
# Config file is *not* specified - use the default
cfg_filename = base_dir.joinpath('config.yaml').resolve()
if not cfg_filename.exists():
if not cfg_filename.exists() and create:
print("InvenTree configuration file 'config.yaml' not found - creating default file")
cfg_template = base_dir.joinpath("config_template.yaml")
@ -38,45 +47,159 @@ def get_config_file() -> Path:
return cfg_filename
def get_plugin_file():
"""Returns the path of the InvenTree plugins specification file.
def load_config_data() -> map:
"""Load configuration data from the config file."""
Note: It will be created if it does not already exist!
"""
# Check if the plugin.txt file (specifying required plugins) is specified
PLUGIN_FILE = os.getenv('INVENTREE_PLUGIN_FILE')
cfg_file = get_config_file()
if not PLUGIN_FILE:
# If not specified, look in the same directory as the configuration file
config_dir = get_config_file().parent
PLUGIN_FILE = config_dir.joinpath('plugins.txt')
else:
# Make sure we are using a modern Path object
PLUGIN_FILE = Path(PLUGIN_FILE)
with open(cfg_file, 'r') as cfg:
data = yaml.safe_load(cfg)
if not PLUGIN_FILE.exists():
logger.warning("Plugin configuration file does not exist")
logger.info(f"Creating plugin file at '{PLUGIN_FILE}'")
# If opening the file fails (no write permission, for example), then this will throw an error
PLUGIN_FILE.write_text("# InvenTree Plugins (uses PIP framework to install)\n\n")
return PLUGIN_FILE
return data
def get_setting(environment_var, backup_val, default_value=None):
def get_setting(env_var=None, config_key=None, default_value=None):
"""Helper function for retrieving a configuration setting value.
- First preference is to look for the environment variable
- Second preference is to look for the value of the settings file
- Third preference is the default value
Arguments:
env_var: Name of the environment variable e.g. 'INVENTREE_STATIC_ROOT'
config_key: Key to lookup in the configuration file
default_value: Value to return if first two options are not provided
"""
val = os.getenv(environment_var)
if val is not None:
return val
# First, try to load from the environment variables
if env_var is not None:
val = os.getenv(env_var, None)
if backup_val is not None:
return backup_val
if val is not None:
return val
# Next, try to load from configuration file
if config_key is not None:
cfg_data = load_config_data()
result = None
# Hack to allow 'path traversal' in configuration file
for key in config_key.strip().split('.'):
if type(cfg_data) is not dict or key not in cfg_data:
result = None
break
result = cfg_data[key]
cfg_data = cfg_data[key]
if result is not None:
return result
# Finally, return the default value
return default_value
def get_boolean_setting(env_var=None, config_key=None, default_value=False):
"""Helper function for retreiving a boolean configuration setting"""
return is_true(get_setting(env_var, config_key, default_value))
def get_media_dir(create=True):
"""Return the absolute path for the 'media' directory (where uploaded files are stored)"""
md = get_setting('INVENTREE_MEDIA_ROOT', 'media_root')
if not md:
raise FileNotFoundError('INVENTREE_MEDIA_ROOT not specified')
md = Path(md).resolve()
if create:
md.mkdir(parents=True, exist_ok=True)
return md
def get_static_dir(create=True):
"""Return the absolute path for the 'static' directory (where static files are stored)"""
sd = get_setting('INVENTREE_STATIC_ROOT', 'static_root')
if not sd:
raise FileNotFoundError('INVENTREE_STATIC_ROOT not specified')
sd = Path(sd).resolve()
if create:
sd.mkdir(parents=True, exist_ok=True)
return sd
def get_plugin_file():
"""Returns the path of the InvenTree plugins specification file.
Note: It will be created if it does not already exist!
"""
# Check if the plugin.txt file (specifying required plugins) is specified
plugin_file = get_setting('INVENTREE_PLUGIN_FILE', 'plugin_file')
if not plugin_file:
# If not specified, look in the same directory as the configuration file
config_dir = get_config_file().parent
plugin_file = config_dir.joinpath('plugins.txt')
else:
# Make sure we are using a modern Path object
plugin_file = Path(plugin_file)
if not plugin_file.exists():
logger.warning("Plugin configuration file does not exist - creating default file")
logger.info(f"Creating plugin file at '{plugin_file}'")
# If opening the file fails (no write permission, for example), then this will throw an error
plugin_file.write_text("# InvenTree Plugins (uses PIP framework to install)\n\n")
return plugin_file
def get_secret_key():
"""Return the secret key value which will be used by django.
Following options are tested, in descending order of preference:
A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
C) Look for default key file "secret_key.txt"
D) Create "secret_key.txt" if it does not exist
"""
# Look for environment variable
if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'):
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
return secret_key
# Look for secret key file
if secret_key_file := get_setting('INVENTREE_SECRET_KEY_FILE', 'secret_key_file'):
secret_key_file = Path(secret_key_file).resolve()
else:
# Default location for secret key file
secret_key_file = get_base_dir().joinpath("secret_key.txt").resolve()
if not secret_key_file.exists():
logger.info(f"Generating random key file at '{secret_key_file}'")
# Create a random key file
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(100)])
secret_key_file.write_text(key)
logger.info(f"Loading SECRET_KEY from '{secret_key_file}'")
key_data = secret_key_file.read_text().strip()
return key_data

View File

@ -29,7 +29,7 @@ def log_error(path):
kind, info, data = sys.exc_info()
# Check if the eror is on the ignore list
if kind in settings.IGNORRED_ERRORS:
if kind in settings.IGNORED_ERRORS:
return
Error.objects.create(

View File

@ -157,7 +157,7 @@ class InvenTreeExceptionProcessor(ExceptionProcessor):
kind, info, data = sys.exc_info()
# Check if the eror is on the ignore list
if kind in settings.IGNORRED_ERRORS:
if kind in settings.IGNORED_ERRORS:
return
return super().process_exception(request, exception)

View File

@ -650,13 +650,15 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
users = get_user_model().objects.filter(is_staff=True)
link = InvenTree.helpers.construct_absolute_url(
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
)
context = {
'error': instance,
'name': _('Server Error'),
'message': _('An error has been logged by the server.'),
'link': InvenTree.helpers.construct_absolute_url(
reverse('admin:error_report_error_change', kwargs={'object_id': instance.pk})
)
'link': link
}
common.notifications.trigger_notification(

View File

@ -11,9 +11,7 @@ database setup in this file.
import logging
import os
import random
import socket
import string
import sys
from pathlib import Path
@ -24,22 +22,14 @@ from django.utils.translation import gettext_lazy as _
import moneyed
import sentry_sdk
import yaml
from sentry_sdk.integrations.django import DjangoIntegration
from .config import get_base_dir, get_config_file, get_plugin_file, get_setting
def _is_true(x):
# Shortcut function to determine if a value "looks" like a boolean
return str(x).strip().lower() in ['1', 'y', 'yes', 't', 'true']
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
INVENTREE_DSN = 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
from . import config
from .config import get_boolean_setting, get_setting
# Determine if we are running in "test" mode e.g. "manage.py test"
TESTING = 'test' in sys.argv
# Are enviroment variables manipulated by tests? Needs to be set by testing code
TESTING_ENV = False
@ -47,33 +37,17 @@ TESTING_ENV = False
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Build paths inside the project like this: BASE_DIR.joinpath(...)
BASE_DIR = get_base_dir()
BASE_DIR = config.get_base_dir()
cfg_filename = get_config_file()
with open(cfg_filename, 'r') as cfg:
CONFIG = yaml.safe_load(cfg)
# We will place any config files in the same directory as the config file
config_dir = cfg_filename.parent
# Load configuration data
CONFIG = config.load_config_data()
# Default action is to run the system in Debug mode
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = _is_true(get_setting(
'INVENTREE_DEBUG',
CONFIG.get('debug', True)
))
DOCKER = _is_true(get_setting(
'INVENTREE_DOCKER',
False
))
DEBUG = get_boolean_setting('INVENTREE_DEBUG', 'debug', True)
# Configure logging settings
log_level = get_setting(
'INVENTREE_LOG_LEVEL',
CONFIG.get('log_level', 'WARNING')
)
log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
logging.basicConfig(
level=log_level,
@ -105,78 +79,20 @@ LOGGING = {
# Get a logger instance for this setup file
logger = logging.getLogger("inventree")
"""
Specify a secret key to be used by django.
Following options are tested, in descending order of preference:
A) Check for environment variable INVENTREE_SECRET_KEY => Use raw key data
B) Check for environment variable INVENTREE_SECRET_KEY_FILE => Load key data from file
C) Look for default key file "secret_key.txt"
d) Create "secret_key.txt" if it does not exist
"""
if secret_key := os.getenv("INVENTREE_SECRET_KEY"):
# Secret key passed in directly
SECRET_KEY = secret_key.strip() # pragma: no cover
logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY") # pragma: no cover
else:
# Secret key passed in by file location
key_file = os.getenv("INVENTREE_SECRET_KEY_FILE")
if key_file:
key_file = Path(key_file).resolve() # pragma: no cover
else:
# default secret key location
key_file = BASE_DIR.joinpath("secret_key.txt").resolve()
if not key_file.exists(): # pragma: no cover
logger.info(f"Generating random key file at '{key_file}'")
# Create a random key file
options = string.digits + string.ascii_letters + string.punctuation
key = ''.join([random.choice(options) for i in range(100)])
key_file.write_text(key)
logger.info(f"Loading SECRET_KEY from '{key_file}'")
try:
SECRET_KEY = open(key_file, "r").read().strip()
except Exception: # pragma: no cover
logger.exception(f"Couldn't load keyfile {key_file}")
sys.exit(-1)
# Load SECRET_KEY
SECRET_KEY = config.get_secret_key()
# The filesystem location for served static files
STATIC_ROOT = Path(
get_setting(
'INVENTREE_STATIC_ROOT',
CONFIG.get('static_root', None)
)
).resolve()
STATIC_ROOT = config.get_static_dir()
if STATIC_ROOT is None: # pragma: no cover
print("ERROR: INVENTREE_STATIC_ROOT directory not defined")
sys.exit(1)
else:
# Ensure the root really is availalble
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
# The filesystem location for served static files
MEDIA_ROOT = Path(
get_setting(
'INVENTREE_MEDIA_ROOT',
CONFIG.get('media_root', None)
)
).resolve()
if MEDIA_ROOT is None: # pragma: no cover
print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined")
sys.exit(1)
else:
# Ensure the root really is availalble
MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
# The filesystem location for uploaded meadia files
MEDIA_ROOT = config.get_media_dir()
# List of allowed hosts (default = allow all)
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
ALLOWED_HOSTS = get_setting(
config_key='allowed_hosts',
default_value=['*']
)
# Cross Origin Resource Sharing (CORS) options
@ -184,13 +100,15 @@ ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
CORS_URLS_REGEX = r'^/api/.*$'
# Extract CORS options from configuration file
cors_opt = CONFIG.get('cors', None)
CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
config_key='cors.allow_all',
default_value=False,
)
if cors_opt:
CORS_ORIGIN_ALLOW_ALL = cors_opt.get('allow_all', False)
if not CORS_ORIGIN_ALLOW_ALL:
CORS_ORIGIN_WHITELIST = cors_opt.get('whitelist', []) # pragma: no cover
CORS_ORIGIN_WHITELIST = get_setting(
config_key='cors.whitelist',
default_value=[]
)
# Web URL endpoint for served static files
STATIC_URL = '/static/'
@ -214,12 +132,6 @@ STATIC_COLOR_THEMES_DIR = STATIC_ROOT.joinpath('css', 'color-themes')
# Web URL endpoint for served media files
MEDIA_URL = '/media/'
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
# Application definition
INSTALLED_APPS = [
@ -320,6 +232,9 @@ INTERNAL_IPS = [
'127.0.0.1',
]
# Internal flag to determine if we are running in docker mode
DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False)
if DOCKER: # pragma: no cover
# Internal IP addresses are different when running under docker
hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname())
@ -334,7 +249,8 @@ if DEBUG:
# Base URL for admin pages (default="admin")
INVENTREE_ADMIN_URL = get_setting(
'INVENTREE_ADMIN_URL',
CONFIG.get('admin_url', 'admin'),
config_key='admin_url',
default_value='admin'
)
ROOT_URLCONF = 'InvenTree.urls'
@ -498,7 +414,7 @@ if "postgres" in db_engine: # pragma: no cover
# long to connect to the database server
# # seconds, 2 is minium allowed by libpq
db_options["connect_timeout"] = int(
os.getenv("INVENTREE_DB_TIMEOUT", 2)
get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
)
# Setup TCP keepalive
@ -509,23 +425,27 @@ if "postgres" in db_engine: # pragma: no cover
# # 0 - TCP Keepalives disabled; 1 - enabled
if "keepalives" not in db_options:
db_options["keepalives"] = int(
os.getenv("INVENTREE_DB_TCP_KEEPALIVES", "1")
get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
)
# # Seconds after connection is idle to send keep alive
# Seconds after connection is idle to send keep alive
if "keepalives_idle" not in db_options:
db_options["keepalives_idle"] = int(
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_IDLE", "1")
get_setting('INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1)
)
# # Seconds after missing ACK to send another keep alive
# Seconds after missing ACK to send another keep alive
if "keepalives_interval" not in db_options:
db_options["keepalives_interval"] = int(
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", "1")
get_setting("INVENTREE_DB_TCP_KEEPALIVES_INTERVAL", "database.tcp_keepalives_internal", "1")
)
# # Number of missing ACKs before we close the connection
# Number of missing ACKs before we close the connection
if "keepalives_count" not in db_options:
db_options["keepalives_count"] = int(
os.getenv("INVENTREE_DB_TCP_KEEPALIVES_COUNT", "5")
get_setting("INVENTREE_DB_TCP_KEEPALIVES_COUNT", "database.tcp_keepalives_count", "5")
)
# # Milliseconds for how long pending data should remain unacked
# by the remote server
# TODO: Supported starting in PSQL 11
@ -538,17 +458,11 @@ if "postgres" in db_engine: # pragma: no cover
# https://www.postgresql.org/docs/devel/transaction-iso.html
# https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
if "isolation_level" not in db_options:
serializable = _is_true(
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false")
)
db_options["isolation_level"] = (
ISOLATION_LEVEL_SERIALIZABLE
if serializable
else ISOLATION_LEVEL_READ_COMMITTED
)
serializable = get_boolean_setting('INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False)
db_options["isolation_level"] = ISOLATION_LEVEL_SERIALIZABLE if serializable else ISOLATION_LEVEL_READ_COMMITTED
# Specific options for MySql / MariaDB backend
if "mysql" in db_engine: # pragma: no cover
elif "mysql" in db_engine: # pragma: no cover
# TODO TCP time outs and keepalives
# MariaDB's default isolation level is Repeatable Read which is
@ -558,15 +472,11 @@ if "mysql" in db_engine: # pragma: no cover
# https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level
# https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
if "isolation_level" not in db_options:
serializable = _is_true(
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false")
)
db_options["isolation_level"] = (
"serializable" if serializable else "read committed"
)
serializable = get_boolean_setting('INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False)
db_options["isolation_level"] = "serializable" if serializable else "read committed"
# Specific options for sqlite backend
if "sqlite" in db_engine:
elif "sqlite" in db_engine:
# TODO: Verify timeouts are not an issue because no network is involved for SQLite
# SQLite's default isolation level is Serializable due to SQLite's
@ -591,13 +501,11 @@ DATABASES = {
'default': db_config
}
_cache_config = CONFIG.get("cache", {})
_cache_host = _cache_config.get("host", os.getenv("INVENTREE_CACHE_HOST"))
_cache_port = _cache_config.get(
"port", os.getenv("INVENTREE_CACHE_PORT", "6379")
)
# Cache configuration
cache_host = get_setting('INVENTREE_CACHE_HOST', 'cache.host', None)
cache_port = get_setting('INVENTREE_CACHE_PORT', 'cache.port', '6379')
if _cache_host: # pragma: no cover
if cache_host: # pragma: no cover
# We are going to rely upon a possibly non-localhost for our cache,
# so don't wait too long for the cache as nothing in the cache should be
# irreplacable.
@ -606,7 +514,7 @@ if _cache_host: # pragma: no cover
"SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
"SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")),
"CONNECTION_POOL_KWARGS": {
"socket_keepalive": _is_true(
"socket_keepalive": config.is_true(
os.getenv("CACHE_TCP_KEEPALIVE", "1")
),
"socket_keepalive_options": {
@ -628,7 +536,7 @@ if _cache_host: # pragma: no cover
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{_cache_host}:{_cache_port}/0",
"LOCATION": f"redis://{cache_host}:{cache_port}/0",
"OPTIONS": _cache_options,
},
}
@ -639,17 +547,11 @@ else:
},
}
try:
# 4 background workers seems like a sensible default
background_workers = int(os.environ.get('INVENTREE_BACKGROUND_WORKERS', 4))
except ValueError: # pragma: no cover
background_workers = 4
# django-q configuration
# django-q background worker configuration
Q_CLUSTER = {
'name': 'InvenTree',
'workers': background_workers,
'timeout': 90,
'workers': int(get_setting('INVENTREE_BACKGROUND_WORKERS', 'background.workers', 4)),
'timeout': int(get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)),
'retry': 120,
'queue_limit': 50,
'bulk': 10,
@ -657,7 +559,7 @@ Q_CLUSTER = {
'sync': False,
}
if _cache_host: # pragma: no cover
if cache_host: # pragma: no cover
# If using external redis cache, make the cache the broker for Django Q
# as well
Q_CLUSTER["django_redis"] = "worker"
@ -698,8 +600,7 @@ if type(EXTRA_URL_SCHEMES) not in [list]: # pragma: no cover
# Internationalization
# https://docs.djangoproject.com/en/dev/topics/i18n/
LANGUAGE_CODE = CONFIG.get('language', 'en-us')
LANGUAGE_CODE = get_setting('INVENTREE_LANGUAGE', 'language', 'en-us')
# If a new language translation is supported, it must be added here
LANGUAGES = [
@ -730,7 +631,7 @@ LANGUAGES = [
]
# Testing interface translations
if get_setting('TEST_TRANSLATIONS', False): # pragma: no cover
if get_boolean_setting('TEST_TRANSLATIONS', default_value=False): # pragma: no cover
# Set default language
LANGUAGE_CODE = 'xx'
@ -762,68 +663,29 @@ for currency in CURRENCIES:
print(f"Currency code '{currency}' is not supported")
sys.exit(1)
# Custom currency exchange backend
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeExchange'
# Extract email settings from the config file
email_config = CONFIG.get('email', {})
# Email configuration options
EMAIL_BACKEND = get_setting('INVENTREE_EMAIL_BACKEND', 'email.backend', 'django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = get_setting('INVENTREE_EMAIL_HOST', 'email.host', '')
EMAIL_PORT = int(get_setting('INVENTREE_EMAIL_PORT', 'email.port', 25))
EMAIL_HOST_USER = get_setting('INVENTREE_EMAIL_USERNAME', 'email.username', '')
EMAIL_HOST_PASSWORD = get_setting('INVENTREE_EMAIL_PASSWORD', 'email.password', '')
EMAIL_SUBJECT_PREFIX = get_setting('INVENTREE_EMAIL_PREFIX', 'email.prefix', '[InvenTree] ')
EMAIL_USE_TLS = get_boolean_setting('INVENTREE_EMAIL_TLS', 'email.tls', False)
EMAIL_USE_SSL = get_boolean_setting('INVENTREE_EMAIL_SSL', 'email.ssl', False)
EMAIL_BACKEND = get_setting(
'INVENTREE_EMAIL_BACKEND',
email_config.get('backend', 'django.core.mail.backends.smtp.EmailBackend')
)
# Email backend settings
EMAIL_HOST = get_setting(
'INVENTREE_EMAIL_HOST',
email_config.get('host', '')
)
EMAIL_PORT = get_setting(
'INVENTREE_EMAIL_PORT',
email_config.get('port', 25)
)
EMAIL_HOST_USER = get_setting(
'INVENTREE_EMAIL_USERNAME',
email_config.get('username', ''),
)
EMAIL_HOST_PASSWORD = get_setting(
'INVENTREE_EMAIL_PASSWORD',
email_config.get('password', ''),
)
DEFAULT_FROM_EMAIL = get_setting(
'INVENTREE_EMAIL_SENDER',
email_config.get('sender', ''),
)
EMAIL_SUBJECT_PREFIX = '[InvenTree] '
DEFUALT_FROM_EMAIL = get_setting('INVENTREE_EMAIL_SENDER', 'email.sender', '')
EMAIL_USE_LOCALTIME = False
EMAIL_USE_TLS = get_setting(
'INVENTREE_EMAIL_TLS',
email_config.get('tls', False),
)
EMAIL_USE_SSL = get_setting(
'INVENTREE_EMAIL_SSL',
email_config.get('ssl', False),
)
EMAIL_TIMEOUT = 60
LOCALE_PATHS = (
BASE_DIR.joinpath('locale/'),
)
TIME_ZONE = get_setting(
'INVENTREE_TIMEZONE',
CONFIG.get('timezone', 'UTC')
)
TIME_ZONE = get_setting('INVENTREE_TIMEZONE', 'timezone', 'UTC')
USE_I18N = True
@ -856,8 +718,8 @@ SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
SOCIALACCOUNT_STORE_TOKENS = True
# settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', CONFIG.get('login_confirm_days', 3))
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', CONFIG.get('login_attempts', 5))
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', 'login_confirm_days', 3)
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', 'login_attempts', 5)
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
ACCOUNT_PREVENT_ENUMERATION = True
@ -877,8 +739,8 @@ SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'
# login settings
REMOTE_LOGIN = get_setting('INVENTREE_REMOTE_LOGIN', CONFIG.get('remote_login', False))
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', CONFIG.get('remote_login_header', 'REMOTE_USER'))
REMOTE_LOGIN = get_boolean_setting('INVENTREE_REMOTE_LOGIN', 'remote_login_enabled', False)
REMOTE_LOGIN_HEADER = get_setting('INVENTREE_REMOTE_LOGIN_HEADER', 'remote_login_header', 'REMOTE_USER')
# Markdownify configuration
# Ref: https://django-markdownify.readthedocs.io/en/latest/settings.html
@ -909,11 +771,12 @@ MARKDOWNIFY = {
}
}
# Error reporting
SENTRY_ENABLED = get_setting('INVENTREE_SENTRY_ENABLED', CONFIG.get('sentry_enabled', False))
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', CONFIG.get('sentry_dsn', INVENTREE_DSN))
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', CONFIG.get('sentry_sample_rate', 0.1)))
# sentry.io integration for error reporting
SENTRY_ENABLED = get_boolean_setting('INVENTREE_SENTRY_ENABLED', 'sentry_enabled', False)
# Default Sentry DSN (can be overriden if user wants custom sentry integration)
INVENTREE_DSN = 'https://3928ccdba1d34895abde28031fd00100@o378676.ingest.sentry.io/6494600'
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', 'sentry_dsn', INVENTREE_DSN)
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', 'sentry_sample_rate', 0.1))
if SENTRY_ENABLED and SENTRY_DSN: # pragma: no cover
sentry_sdk.init(
@ -932,7 +795,7 @@ if SENTRY_ENABLED and SENTRY_DSN: # pragma: no cover
sentry_sdk.set_tag(f'inventree_{key}', val)
# In-database error logging
IGNORRED_ERRORS = [
IGNORED_ERRORS = [
Http404
]
@ -941,33 +804,29 @@ MAINTENANCE_MODE_RETRY_AFTER = 60
MAINTENANCE_MODE_STATE_BACKEND = 'maintenance_mode.backends.DefaultStorageBackend'
# Are plugins enabled?
PLUGINS_ENABLED = _is_true(get_setting(
'INVENTREE_PLUGINS_ENABLED',
CONFIG.get('plugins_enabled', False),
))
PLUGINS_ENABLED = get_boolean_setting('INVENTREE_PLUGINS_ENABLED', 'plugins_enabled', False)
PLUGIN_FILE = get_plugin_file()
PLUGIN_FILE = config.get_plugin_file()
# Plugin test settings
PLUGIN_TESTING = get_setting('PLUGIN_TESTING', TESTING) # are plugins beeing tested?
PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing?
PLUGIN_TESTING = CONFIG.get('PLUGIN_TESTING', TESTING) # are plugins beeing tested?
PLUGIN_TESTING_SETUP = CONFIG.get('PLUGIN_TESTING_SETUP', False) # load plugins from setup hooks in testing?
PLUGIN_TESTING_EVENTS = False # Flag if events are tested right now
PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried?
PLUGIN_RETRY = CONFIG.get('PLUGIN_RETRY', 5) # how often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
# User interface customization values
CUSTOMIZE = get_setting(
'INVENTREE_CUSTOMIZE',
CONFIG.get('customize', {}),
{}
)
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
CUSTOM_LOGO = get_setting(
'INVENTREE_CUSTOM_LOGO',
CUSTOMIZE.get('logo', False)
)
CUSTOM_LOGO = get_setting('INVENTREE_CUSTOM_LOGO', 'customize.logo', None)
# check that the logo-file exsists in media
if CUSTOM_LOGO and not default_storage.exists(CUSTOM_LOGO): # pragma: no cover
logger.warning(f"The custom logo file '{CUSTOM_LOGO}' could not be found in the default media storage")
CUSTOM_LOGO = False
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")

View File

@ -84,7 +84,7 @@ class MiddlewareTests(InvenTreeTestCase):
log_error('testpath')
# Test setup without ignored errors
settings.IGNORRED_ERRORS = []
settings.IGNORED_ERRORS = []
response = self.client.get(reverse('part-detail', kwargs={'pk': 9999}))
self.assertEqual(response.status_code, 404)
check(1)

View File

@ -13,7 +13,7 @@ import common.models
from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version
INVENTREE_SW_VERSION = "0.8.0 dev"
INVENTREE_SW_VERSION = "0.8.0"
def inventreeInstanceName():

View File

@ -2,12 +2,12 @@
from django.db import migrations
from common.models import InvenTreeSetting
from InvenTree.settings import get_setting, CONFIG
from InvenTree.config import get_setting
def set_default_currency(apps, schema_editor):
""" migrate the currency setting from config.yml to db """
# get value from settings-file
base_currency = get_setting('INVENTREE_BASE_CURRENCY', CONFIG.get('base_currency', 'USD'))
base_currency = get_setting('INVENTREE_BASE_CURRENCY', 'base_currency', 'USD')
# write to database
InvenTreeSetting.set_setting('INVENTREE_DEFAULT_CURRENCY', base_currency, None, create=True)

View File

@ -1,10 +1,12 @@
"""JSON serializers for common components."""
from django.urls import reverse
from rest_framework import serializers
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
NotificationMessage)
from InvenTree.helpers import get_objectreference
from InvenTree.helpers import construct_absolute_url, get_objectreference
from InvenTree.serializers import InvenTreeModelSerializer
@ -157,7 +159,22 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
def get_target(self, obj):
"""Function to resolve generic object reference to target."""
return get_objectreference(obj, 'target_content_type', 'target_object_id')
target = get_objectreference(obj, 'target_content_type', 'target_object_id')
if 'link' not in target:
# Check if objekt has an absolute_url function
if hasattr(obj.target_object, 'get_absolute_url'):
target['link'] = obj.target_object.get_absolute_url()
else:
# check if user is staff - link to admin
request = self.context['request']
if request.user and request.user.is_staff:
meta = obj.target_object._meta
target['link'] = construct_absolute_url(reverse(
f'admin:{meta.db_table}_change',
kwargs={'object_id': obj.target_object_id}
))
return target
def get_source(self, obj):
"""Function to resolve generic object reference to source."""

View File

@ -1,7 +1,6 @@
# Database backend selection - Configure backend database settings
# Ref: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-DATABASES
# Specify database parameters below as they appear in the Django docs
# Documentation: https://inventree.readthedocs.io/en/latest/start/config/
# Note: Database configuration options can also be specified from environmental variables,
# with the prefix INVENTREE_DB_
@ -44,20 +43,32 @@ database:
# ENGINE: sqlite3
# NAME: '/home/inventree/database.sqlite3'
# Set debug to False to run in production mode
# Use the environment variable INVENTREE_DEBUG
debug: True
# Configure the system logging level
# Use environment variable INVENTREE_LOG_LEVEL
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: WARNING
# Select default system language (default is 'en-us')
# Use the environment variable INVENTREE_LANGUAGE
language: en-us
# System time-zone (default is UTC)
# Reference: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# Select an option from the "TZ database name" column
# Use the environment variable INVENTREE_TIMEZONE
timezone: UTC
# Base currency code
# Base currency code (or use env var INVENTREE_BASE_CURRENCY)
base_currency: USD
# List of currencies supported by default.
# Add other currencies here to allow use in InvenTree
# Add new user on first startup
#admin_user: admin
#admin_email: info@example.com
#admin_password: inventree
# List of currencies supported by default. Add other currencies here to allow use in InvenTree
currencies:
- AUD
- CAD
@ -70,15 +81,6 @@ currencies:
# Email backend configuration
# Ref: https://docs.djangoproject.com/en/dev/topics/email/
# Available options:
# host: Email server host address
# port: Email port
# username: Account username
# password: Account password
# prefix: Email subject prefix
# tls: Enable TLS support
# ssl: Enable SSL support
# Alternatively, these options can all be set using environment variables,
# with the INVENTREE_EMAIL_ prefix:
# e.g. INVENTREE_EMAIL_HOST / INVENTREE_EMAIL_PORT / INVENTREE_EMAIL_USERNAME
@ -94,31 +96,17 @@ email:
tls: False
ssl: False
# Set debug to False to run in production mode
# Use the environment variable INVENTREE_DEBUG
debug: True
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
# Note: This will only be displayed if DEBUG mode is enabled,
# and only if InvenTree is accessed from a local IP (127.0.0.1)
debug_toolbar: False
# Set sentry_enabled to True to report errors back to the maintainers
# Use the environment variable INVENTREE_SENTRY_ENABLED
# sentry_enabled: True
# Set sentry_dsn to your custom DSN if you want to use your own instance for error reporting
# Use the environment variable INVENTREE_SENTRY_DSN
# sentry_dsn: https://custom@custom.ingest.sentry.io/custom
# Set sentry,dsn to your custom DSN if you want to use your own instance for error reporting
sentry_enabled: False
#sentry_sample_rate: 0.1
#sentry_dsn: https://custom@custom.ingest.sentry.io/custom
# Set this variable to True to enable InvenTree Plugins
# Alternatively, use the environment variable INVENTREE_PLUGINS_ENABLED
plugins_enabled: False
# Configure the system logging level
# Use environment variable INVENTREE_LOG_LEVEL
# Options: DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: WARNING
#plugin_file: /path/to/plugins.txt
#plugin_dir: /path/to/plugins/
# Allowed hosts (see ALLOWED_HOSTS in Django settings documentation)
# A list of strings representing the host/domain names that this Django site can serve.
@ -138,14 +126,15 @@ cors:
# - https://sub.example.com
# MEDIA_ROOT is the local filesystem location for storing uploaded files
# By default, it is stored under /home/inventree/data/media
# Use environment variable INVENTREE_MEDIA_ROOT
media_root: '/home/inventree/data/media'
# media_root: '/home/inventree/data/media'
# STATIC_ROOT is the local filesystem location for storing static files
# By default, it is stored under /home/inventree/data/static
# Use environment variable INVENTREE_STATIC_ROOT
static_root: '/home/inventree/data/static'
# static_root: '/home/inventree/data/static'
# Background worker options
background:
workers: 4
timeout: 90
# Optional URL schemes to allow in URL fields
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
@ -156,25 +145,14 @@ static_root: '/home/inventree/data/static'
# - ssh
# Login configuration
# How long do confirmation mail last?
# Use environment variable INVENTREE_LOGIN_CONFIRM_DAYS
#login_confirm_days: 3
# How many wrong login attempts are permitted?
# Use environment variable INVENTREE_LOGIN_ATTEMPTS
#login_attempts: 5
login_confirm_days: 3
login_attempts: 5
# Remote / proxy login
# These settings can introduce security problems if configured incorrectly. Please read
# https://docs.djangoproject.com/en/4.0/howto/auth-remote-user/ for more details
# Use environment variable INVENTREE_REMOTE_LOGIN
# remote_login: True
# Use environment variable INVENTREE_REMOTE_LOGIN_HEADER
# remote_login_header: REMOTE_USER
# Add new user on first startup
#admin_user: admin
#admin_email: info@example.com
#admin_password: inventree
remote_login_enabled: False
remote_login_header: REMOTE_USER
# Permit custom authentication backends
#authentication_backends:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,8 @@ from django.utils.text import slugify
from maintenance_mode.core import (get_maintenance_mode, maintenance_mode_on,
set_maintenance_mode)
from InvenTree.config import get_setting
from .helpers import (IntegrationPluginError, get_plugins, handle_error,
log_error)
from .plugin import InvenTreePlugin
@ -199,7 +201,7 @@ class PluginsRegistry:
if settings.TESTING:
custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
else:
custom_dirs = os.getenv('INVENTREE_PLUGIN_DIR', None)
custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
# Load from user specified directories (unless in testing mode)
dirs.append('plugins')

View File

@ -44,7 +44,7 @@ def get_existing_release_tags():
return tags
def check_version_number(version_string):
def check_version_number(version_string, allow_duplicate=False):
"""Check the provided version number.
Returns True if the provided version is the 'newest' InvenTree release
@ -67,7 +67,7 @@ def check_version_number(version_string):
highest_release = True
for release in existing:
if release == version_tuple:
if release == version_tuple and not allow_duplicate:
raise ValueError(f"Duplicate release '{version_string}' exists!")
if release > version_tuple:
@ -108,7 +108,9 @@ if __name__ == '__main__':
print(f"InvenTree Version: '{version}'")
highest_release = check_version_number(version)
# Check version number and look for existing versions
# Note that on a 'tag' (release) we *must* allow duplicate versions, as this *is* the version that has just been released
highest_release = check_version_number(version, allow_duplicate=GITHUB_REF_TYPE == 'tag')
# Determine which docker tag we are going to use
docker_tags = None

View File

@ -1,9 +1,10 @@
# Base python requirements for docker containers
# Basic package requirements
invoke>=1.4.0 # Invoke build tool
pyyaml>=6.0
setuptools==60.0.5
wheel>=0.37.0
invoke>=1.4.0 # Invoke build tool
# Database links
psycopg2>=2.9.1

View File

@ -4,6 +4,7 @@ import json
import os
import pathlib
import re
import shutil
import sys
from pathlib import Path
@ -522,6 +523,8 @@ def test(c, database=None):
def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset"):
"""Setup a testing enviroment."""
from InvenTree.InvenTree.config import get_media_dir
if not ignore_update:
update(c)
@ -540,8 +543,16 @@ def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset")
migrate(c)
# Load data
print("Loading data ...")
print("Loading database records ...")
import_records(c, filename=f'{path}/inventree_data.json', clear=True)
# Copy media files
print("Copying media files ...")
src = Path(path).joinpath('media').resolve()
dst = get_media_dir()
shutil.copytree(src, dst, dirs_exist_ok=True)
print("Done setting up test enviroment...")
print("========================================")