2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[WIP] Site ID Fixes (#6390)

* Fix docs for INVENTREE_SITE_URL

* Adjust default SITE_ID

* Optional support for multi-site

- Disable by default

* Prevent site setting from being changed if set by config parameter

* Update site url setting on server launch

* Update log messages

* Update RULESET_MODELS

* Update unit tests

* More fixes for unit tests

* Update docs

* Update SSO image
This commit is contained in:
Oliver 2024-02-03 22:51:29 +11:00 committed by GitHub
parent 538ff9be7b
commit 5bc00298c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 361 additions and 237 deletions

View File

@ -58,6 +58,7 @@ class InvenTreeConfig(AppConfig):
# Let the background worker check for migrations # Let the background worker check for migrations
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
self.update_site_url()
self.collect_notification_methods() self.collect_notification_methods()
self.collect_state_transition_methods() self.collect_state_transition_methods()
@ -223,6 +224,46 @@ class InvenTreeConfig(AppConfig):
except Exception as e: except Exception as e:
logger.exception('Error updating exchange rates: %s (%s)', e, type(e)) logger.exception('Error updating exchange rates: %s (%s)', e, type(e))
def update_site_url(self):
"""Update the site URL setting.
- If a fixed SITE_URL is specified (via configuration), it should override the INVENTREE_BASE_URL setting
- If multi-site support is enabled, update the site URL for the current site
"""
import common.models
if not InvenTree.ready.canAppAccessDatabase():
return
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
return
if settings.SITE_URL:
try:
if (
common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
!= settings.SITE_URL
):
common.models.InvenTreeSetting.set_setting(
'INVENTREE_BASE_URL', settings.SITE_URL
)
logger.info('Updated INVENTREE_SITE_URL to %s', settings.SITE_URL)
except Exception:
pass
# If multi-site support is enabled, update the site URL for the current site
try:
from django.contrib.sites.models import Site
site = Site.objects.get_current()
site.domain = settings.SITE_URL
site.save()
logger.info('Updated current site URL to %s', settings.SITE_URL)
except Exception:
pass
def add_user_on_startup(self): def add_user_on_startup(self):
"""Add a user on startup.""" """Add a user on startup."""
# stop if checks were already created # stop if checks were already created

View File

@ -72,7 +72,7 @@ def user_roles(request):
roles = {} roles = {}
for role in RuleSet.RULESET_MODELS.keys(): for role in RuleSet.get_ruleset_models().keys():
permissions = {} permissions = {}
for perm in ['view', 'add', 'change', 'delete']: for perm in ['view', 'add', 'change', 'delete']:

View File

@ -6,7 +6,6 @@ from urllib.parse import urlencode
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -23,6 +22,7 @@ from crispy_forms.layout import Field, Layout
from dj_rest_auth.registration.serializers import RegisterSerializer from dj_rest_auth.registration.serializers import RegisterSerializer
from rest_framework import serializers from rest_framework import serializers
import InvenTree.helpers_model
import InvenTree.sso import InvenTree.sso
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
@ -293,7 +293,8 @@ class CustomUrlMixin:
def get_email_confirmation_url(self, request, emailconfirmation): def get_email_confirmation_url(self, request, emailconfirmation):
"""Custom email confirmation (activation) url.""" """Custom email confirmation (activation) url."""
url = reverse('account_confirm_email', args=[emailconfirmation.key]) url = reverse('account_confirm_email', args=[emailconfirmation.key])
return Site.objects.get_current().domain + url
return InvenTree.helpers_model.construct_absolute_url(url)
class CustomAccountAdapter( class CustomAccountAdapter(

View File

@ -34,47 +34,59 @@ def getSetting(key, backup_value=None):
return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value) return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value)
def construct_absolute_url(*arg, **kwargs): def get_base_url(request=None):
"""Construct (or attempt to construct) an absolute URL from a relative URL. """Return the base URL for the InvenTree server.
This is useful when (for example) sending an email to a user with a link The base URL is determined in the following order of decreasing priority:
to something in the InvenTree web framework.
A URL is constructed in the following order: 1. If a request object is provided, use the request URL
1. If settings.SITE_URL is set (e.g. in the Django settings), use that 2. Multi-site is enabled, and the current site has a valid URL
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that 3. If settings.SITE_URL is set (e.g. in the Django settings), use that
3. Otherwise, use the current request URL (if available) 4. If the InvenTree setting INVENTREE_BASE_URL is set, use that
""" """
relative_url = '/'.join(arg) # Check if a request is provided
if request:
return request.build_absolute_uri('/')
# If a site URL is provided, use that # Check if multi-site is enabled
site_url = getattr(settings, 'SITE_URL', None)
if not site_url:
# Otherwise, try to use the InvenTree setting
try: try:
site_url = common.models.InvenTreeSetting.get_setting( from django.contrib.sites.models import Site
return Site.objects.get_current().domain
except (ImportError, RuntimeError):
pass
# Check if a global site URL is provided
if site_url := getattr(settings, 'SITE_URL', None):
return site_url
# Check if a global InvenTree setting is provided
try:
if site_url := common.models.InvenTreeSetting.get_setting(
'INVENTREE_BASE_URL', create=False, cache=False 'INVENTREE_BASE_URL', create=False, cache=False
) ):
return site_url
except (ProgrammingError, OperationalError): except (ProgrammingError, OperationalError):
pass pass
if not site_url: # No base URL available
# Otherwise, try to use the current request return ''
request = kwargs.get('request', None)
if request:
site_url = request.build_absolute_uri('/')
if not site_url:
# No site URL available, return the relative URL
return relative_url
return urljoin(site_url, relative_url)
def get_base_url(**kwargs): def construct_absolute_url(*arg, base_url=None, request=None):
"""Return the base URL for the InvenTree server.""" """Construct (or attempt to construct) an absolute URL from a relative URL.
return construct_absolute_url('', **kwargs)
Args:
*arg: The relative URL to construct
base_url: The base URL to use for the construction (if not provided, will attempt to determine from settings)
request: The request object to use for the construction (optional)
"""
relative_url = '/'.join(arg)
if not base_url:
base_url = get_base_url(request=request)
return urljoin(base_url, relative_url)
def download_image_from_url(remote_url, timeout=2.5): def download_image_from_url(remote_url, timeout=2.5):

View File

@ -2,7 +2,6 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.mail import send_mail from django.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
@ -13,18 +12,20 @@ from rest_framework import serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
import InvenTree.version
def send_simple_login_email(user, link): def send_simple_login_email(user, link):
"""Send an email with the login link to this user.""" """Send an email with the login link to this user."""
site = Site.objects.get_current() site_name = InvenTree.version.inventreeInstanceName()
context = {'username': user.username, 'site_name': site.name, 'link': link} context = {'username': user.username, 'site_name': site_name, 'link': link}
email_plaintext_message = render_to_string( email_plaintext_message = render_to_string(
'InvenTree/user_simple_login.txt', context 'InvenTree/user_simple_login.txt', context
) )
send_mail( send_mail(
_(f'[{site.name}] Log in to the app'), _(f'[{site_name}] Log in to the app'),
email_plaintext_message, email_plaintext_message,
settings.DEFAULT_FROM_EMAIL, settings.DEFAULT_FROM_EMAIL,
[user.email], [user.email],

View File

@ -7,7 +7,6 @@ from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -26,7 +25,7 @@ from taggit.serializers import TaggitSerializer
import common.models as common_models import common.models as common_models
from common.settings import currency_code_default, currency_code_mappings from common.settings import currency_code_default, currency_code_mappings
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
from InvenTree.helpers_model import download_image_from_url from InvenTree.helpers_model import download_image_from_url, get_base_url
class InvenTreeMoneySerializer(MoneyField): class InvenTreeMoneySerializer(MoneyField):
@ -445,19 +444,23 @@ class UserCreateSerializer(ExendedUserSerializer):
def create(self, validated_data): def create(self, validated_data):
"""Send an e email to the user after creation.""" """Send an e email to the user after creation."""
base_url = get_base_url()
instance = super().create(validated_data) instance = super().create(validated_data)
# Make sure the user cannot login until they have set a password # Make sure the user cannot login until they have set a password
instance.set_unusable_password() instance.set_unusable_password()
# Send the user an onboarding email (from current site)
current_site = Site.objects.get_current()
domain = current_site.domain
instance.email_user(
subject=_(f'Welcome to {current_site.name}'),
message = _( message = _(
f'Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain}).' 'Your account has been created.\n\nPlease use the password reset function to login'
),
) )
if base_url:
message += f'\nURL: {base_url}'
# Send the user an onboarding email (from current site)
instance.email_user(subject=_('Welcome to InvenTree'), message=message)
return instance return instance

View File

@ -978,13 +978,30 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4'
# Use database transactions when importing / exporting data # Use database transactions when importing / exporting data
IMPORT_EXPORT_USE_TRANSACTIONS = True IMPORT_EXPORT_USE_TRANSACTIONS = True
SITE_ID = 1 # Site URL can be specified statically, or via a run-time setting
SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
if SITE_URL:
logger.info('Using Site URL: %s', SITE_URL)
# Check that the site URL is valid
validator = URLValidator()
validator(SITE_URL)
# Enable or disable multi-site framework
SITE_MULTI = get_boolean_setting('INVENTREE_SITE_MULTI', 'site_multi', False)
# If a SITE_ID is specified
SITE_ID = get_setting('INVENTREE_SITE_ID', 'site_id', 1 if SITE_MULTI else None)
# Load the allauth social backends # Load the allauth social backends
SOCIAL_BACKENDS = get_setting( SOCIAL_BACKENDS = get_setting(
'INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list 'INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list
) )
if not SITE_MULTI:
INSTALLED_APPS.remove('django.contrib.sites')
for app in SOCIAL_BACKENDS: for app in SOCIAL_BACKENDS:
# Ensure that the app starts with 'allauth.socialaccount.providers' # Ensure that the app starts with 'allauth.socialaccount.providers'
social_prefix = 'allauth.socialaccount.providers.' social_prefix = 'allauth.socialaccount.providers.'
@ -1096,16 +1113,6 @@ PLUGIN_RETRY = get_setting(
) # How often should plugin loading be tried? ) # How often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked? PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
# Site URL can be specified statically, or via a run-time setting
SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
if SITE_URL:
logger.info('Site URL: %s', SITE_URL)
# Check that the site URL is valid
validator = URLValidator()
validator(SITE_URL)
# User interface customization values # User interface customization values
CUSTOM_LOGO = get_custom_file( CUSTOM_LOGO = get_custom_file(
'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True 'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True

View File

@ -10,7 +10,6 @@ from unittest import mock
import django.core.exceptions as django_exceptions import django.core.exceptions as django_exceptions
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
@ -372,7 +371,7 @@ class TestHelpers(TestCase):
for url, expected in tests.items(): for url, expected in tests.items():
# Test with supplied base URL # Test with supplied base URL
self.assertEqual( self.assertEqual(
InvenTree.helpers_model.construct_absolute_url(url, site_url=base), InvenTree.helpers_model.construct_absolute_url(url, base_url=base),
expected, expected,
) )
@ -1049,6 +1048,12 @@ class TestInstanceName(InvenTreeTestCase):
self.assertEqual(version.inventreeInstanceTitle(), 'Testing title') self.assertEqual(version.inventreeInstanceTitle(), 'Testing title')
try:
from django.contrib.sites.models import Site
except (ImportError, RuntimeError):
# Multi-site support not enabled
return
# The site should also be changed # The site should also be changed
site_obj = Site.objects.all().order_by('id').first() site_obj = Site.objects.all().order_by('id').first()
self.assertEqual(site_obj.name, 'Testing title') self.assertEqual(site_obj.name, 'Testing title')
@ -1060,9 +1065,18 @@ class TestInstanceName(InvenTreeTestCase):
'INVENTREE_BASE_URL', 'http://127.1.2.3', self.user 'INVENTREE_BASE_URL', 'http://127.1.2.3', self.user
) )
# No further tests if multi-site support is not enabled
if not settings.SITE_MULTI:
return
# The site should also be changed # The site should also be changed
try:
from django.contrib.sites.models import Site
site_obj = Site.objects.all().order_by('id').first() site_obj = Site.objects.all().order_by('id').first()
self.assertEqual(site_obj.domain, 'http://127.1.2.3') self.assertEqual(site_obj.domain, 'http://127.1.2.3')
except Exception:
pass
class TestOffloadTask(InvenTreeTestCase): class TestOffloadTask(InvenTreeTestCase):
@ -1234,7 +1248,7 @@ class MagicLoginTest(InvenTreeTestCase):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, {'status': 'ok'}) self.assertEqual(resp.data, {'status': 'ok'})
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, '[example.com] Log in to the app') self.assertEqual(mail.outbox[0].subject, '[InvenTree] Log in to the app')
# Check that the token is in the email # Check that the token is in the email
self.assertTrue('http://testserver/api/email/login/' in mail.outbox[0].body) self.assertTrue('http://testserver/api/email/login/' in mail.outbox[0].body)

View File

@ -24,7 +24,6 @@ from django.contrib.auth.models import Group, User
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.sites.models import Site
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import AppRegistryNotReady, ValidationError from django.core.exceptions import AppRegistryNotReady, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
@ -101,6 +100,10 @@ class BaseURLValidator(URLValidator):
"""Make sure empty values pass.""" """Make sure empty values pass."""
value = str(value).strip() value = str(value).strip()
# If a configuration level value has been specified, prevent change
if settings.SITE_URL:
raise ValidationError(_('Site URL is locked by configuration'))
if len(value) == 0: if len(value) == 0:
pass pass
@ -647,7 +650,7 @@ class BaseInvenTreeSetting(models.Model):
return value return value
@classmethod @classmethod
def set_setting(cls, key, value, change_user, create=True, **kwargs): def set_setting(cls, key, value, change_user=None, create=True, **kwargs):
"""Set the value of a particular setting. If it does not exist, option to create it. """Set the value of a particular setting. If it does not exist, option to create it.
Args: Args:
@ -1065,6 +1068,15 @@ def settings_group_options():
def update_instance_url(setting): def update_instance_url(setting):
"""Update the first site objects domain to url.""" """Update the first site objects domain to url."""
if not settings.SITE_MULTI:
return
try:
from django.contrib.sites.models import Site
except (ImportError, RuntimeError):
# Multi-site support not enabled
return
site_obj = Site.objects.all().order_by('id').first() site_obj = Site.objects.all().order_by('id').first()
site_obj.domain = setting.value site_obj.domain = setting.value
site_obj.save() site_obj.save()
@ -1072,6 +1084,15 @@ def update_instance_url(setting):
def update_instance_name(setting): def update_instance_name(setting):
"""Update the first site objects name to instance name.""" """Update the first site objects name to instance name."""
if not settings.SITE_MULTI:
return
try:
from django.contrib.sites.models import Site
except (ImportError, RuntimeError):
# Multi-site support not enabled
return
site_obj = Site.objects.all().order_by('id').first() site_obj = Site.objects.all().order_by('id').first()
site_obj.name = setting.value site_obj.name = setting.value
site_obj.save() site_obj.save()

View File

@ -90,8 +90,8 @@ language: en-us
timezone: UTC timezone: UTC
# Base URL for the InvenTree server # Base URL for the InvenTree server
# Use the environment variable INVENTREE_BASE_URL # Use the environment variable INVENTREE_SITE_URL
# base_url: 'http://localhost:8000' # site_url: 'http://localhost:8000'
# Base currency code (or use env var INVENTREE_BASE_CURRENCY) # Base currency code (or use env var INVENTREE_BASE_CURRENCY)
base_currency: USD base_currency: USD

View File

@ -201,7 +201,10 @@ class RuleSet(models.Model):
RULESET_PERMISSIONS = ['view', 'add', 'change', 'delete'] RULESET_PERMISSIONS = ['view', 'add', 'change', 'delete']
RULESET_MODELS = { @staticmethod
def get_ruleset_models():
"""Return a dictionary of models associated with each ruleset."""
ruleset_models = {
'admin': [ 'admin': [
'auth_group', 'auth_group',
'auth_user', 'auth_user',
@ -215,7 +218,6 @@ class RuleSet(models.Model):
'report_salesorderreport', 'report_salesorderreport',
'account_emailaddress', 'account_emailaddress',
'account_emailconfirmation', 'account_emailconfirmation',
'sites_site',
'socialaccount_socialaccount', 'socialaccount_socialaccount',
'socialaccount_socialapp', 'socialaccount_socialapp',
'socialaccount_socialtoken', 'socialaccount_socialtoken',
@ -325,8 +327,16 @@ class RuleSet(models.Model):
], ],
} }
if settings.SITE_MULTI:
ruleset_models['admin'].append('sites_site')
return ruleset_models
# Database models we ignore permission sets for # Database models we ignore permission sets for
RULESET_IGNORE = [ @staticmethod
def get_ruleset_ignore():
"""Return a list of database tables which do not require permissions."""
return [
# Core django models (not user configurable) # Core django models (not user configurable)
'admin_logentry', 'admin_logentry',
'contenttypes_contenttype', 'contenttypes_contenttype',
@ -409,12 +419,12 @@ class RuleSet(models.Model):
return True return True
# If the table does *not* require permissions # If the table does *not* require permissions
if table in cls.RULESET_IGNORE: if table in cls.get_ruleset_ignore():
return True return True
# Work out which roles touch the given table # Work out which roles touch the given table
for role in cls.RULESET_NAMES: for role in cls.RULESET_NAMES:
if table in cls.RULESET_MODELS[role]: if table in cls.get_ruleset_models()[role]:
if check_user_role(user, role, permission): if check_user_role(user, role, permission):
return True return True
@ -474,7 +484,7 @@ class RuleSet(models.Model):
def get_models(self): def get_models(self):
"""Return the database tables / models that this ruleset covers.""" """Return the database tables / models that this ruleset covers."""
return self.RULESET_MODELS.get(self.name, []) return self.get_ruleset_models().get(self.name, [])
def split_model(model): def split_model(model):
@ -669,7 +679,7 @@ def clear_user_role_cache(user):
Args: Args:
user: The User object to be expunged from the cache user: The User object to be expunged from the cache
""" """
for role in RuleSet.RULESET_MODELS.keys(): for role in RuleSet.get_ruleset_models().keys():
for perm in ['add', 'change', 'view', 'delete']: for perm in ['add', 'change', 'view', 'delete']:
key = f'role_{user}_{role}_{perm}' key = f'role_{user}_{role}_{perm}'
cache.delete(key) cache.delete(key)

View File

@ -14,7 +14,7 @@ class RuleSetModelTest(TestCase):
def test_ruleset_models(self): def test_ruleset_models(self):
"""Test that the role rulesets work as intended.""" """Test that the role rulesets work as intended."""
keys = RuleSet.RULESET_MODELS.keys() keys = RuleSet.get_ruleset_models().keys()
# Check if there are any rulesets which do not have models defined # Check if there are any rulesets which do not have models defined
@ -30,16 +30,16 @@ class RuleSetModelTest(TestCase):
if len(extra) > 0: # pragma: no cover if len(extra) > 0: # pragma: no cover
print( print(
'The following rulesets have been improperly added to RULESET_MODELS:' 'The following rulesets have been improperly added to get_ruleset_models():'
) )
for e in extra: for e in extra:
print('-', e) print('-', e)
# Check that each ruleset has models assigned # Check that each ruleset has models assigned
empty = [key for key in keys if len(RuleSet.RULESET_MODELS[key]) == 0] empty = [key for key in keys if len(RuleSet.get_ruleset_models()[key]) == 0]
if len(empty) > 0: # pragma: no cover if len(empty) > 0: # pragma: no cover
print('The following rulesets have empty entries in RULESET_MODELS:') print('The following rulesets have empty entries in get_ruleset_models():')
for e in empty: for e in empty:
print('-', e) print('-', e)
@ -62,8 +62,8 @@ class RuleSetModelTest(TestCase):
assigned_models = set() assigned_models = set()
# Now check that each defined model is a valid table name # Now check that each defined model is a valid table name
for key in RuleSet.RULESET_MODELS.keys(): for key in RuleSet.get_ruleset_models().keys():
models = RuleSet.RULESET_MODELS[key] models = RuleSet.get_ruleset_models()[key]
for m in models: for m in models:
assigned_models.add(m) assigned_models.add(m)
@ -72,7 +72,8 @@ class RuleSetModelTest(TestCase):
for model in available_tables: for model in available_tables:
if ( if (
model not in assigned_models and model not in RuleSet.RULESET_IGNORE model not in assigned_models
and model not in RuleSet.get_ruleset_ignore()
): # pragma: no cover ): # pragma: no cover
missing_models.add(model) missing_models.add(model)
@ -90,7 +91,7 @@ class RuleSetModelTest(TestCase):
for model in assigned_models: for model in assigned_models:
defined_models.add(model) defined_models.add(model)
for model in RuleSet.RULESET_IGNORE: for model in RuleSet.get_ruleset_ignore():
defined_models.add(model) defined_models.add(model)
for model in defined_models: # pragma: no cover for model in defined_models: # pragma: no cover
@ -118,7 +119,7 @@ class RuleSetModelTest(TestCase):
# Check that all permissions have been assigned permissions? # Check that all permissions have been assigned permissions?
permission_set = set() permission_set = set()
for models in RuleSet.RULESET_MODELS.values(): for models in RuleSet.get_ruleset_models().values():
for model in models: for model in models:
permission_set.add(model) permission_set.add(model)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -88,3 +88,15 @@ This can be used to track usage and performance of the InvenTree backend and con
| `tracing.append_http` | `INVENTREE_TRACING_APPEND_HTTP` | Append default url routes (v1) to `tracing.endpoint` | | `tracing.append_http` | `INVENTREE_TRACING_APPEND_HTTP` | Append default url routes (v1) to `tracing.endpoint` |
| `tracing.console` | `INVENTREE_TRACING_CONSOLE` | Print out all exports (additionally) to the console for debugging. Do not use in production | | `tracing.console` | `INVENTREE_TRACING_CONSOLE` | Print out all exports (additionally) to the console for debugging. Do not use in production |
| `tracing.resources` | `INVENTREE_TRACING_RESOURCES` | Add additional resources to all exports. This can be used to add custom tags to the traces. Format as a dict. | | `tracing.resources` | `INVENTREE_TRACING_RESOURCES` | Add additional resources to all exports. This can be used to add custom tags to the traces. Format as a dict. |
## Multi Site Support
If your InvenTree instance is used in a multi-site environment, you can enable multi-site support. Note that supporting multiple sites is well outside the scope of most InvenTree installations. If you know what you are doing, and have a good reason to enable multi-site support, you can do so by setting the `INVENTREE_SITE_MULTI` environment variable to `True`.
!!! tip "Django Documentation"
For more information on multi-site support, refer to the [Django documentation](https://docs.djangoproject.com/en/3.2/ref/contrib/sites/).
| Environment Variable | Config Key | Description | Default |
| --- | --- | --- | --- |
| INVENTREE_SITE_MULTI | site_multi | Enable multiple sites | False |
| INVENTREE_SITE_ID | site_id | Specify a fixed site ID | *Not specified* |

View File

@ -55,6 +55,7 @@ The following basic options are available:
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING | | INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False | | INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
| INVENTREE_TIMEZONE | timezone | Server timezone | UTC | | INVENTREE_TIMEZONE | timezone | Server timezone | UTC |
| INVENTREE_SITE_URL | site_url | Specify a fixed site URL | *Not specified* |
| INVENTREE_ADMIN_ENABLED | admin_enabled | Enable the [django administrator interface](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/) | True | | INVENTREE_ADMIN_ENABLED | admin_enabled | Enable the [django administrator interface](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/) | True |
| INVENTREE_ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin | | INVENTREE_ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin |
| INVENTREE_LANGUAGE | language | Default language | en-us | | INVENTREE_LANGUAGE | language | Default language | en-us |