diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index edb02dbea9..2e90780efa 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -58,6 +58,7 @@ class InvenTreeConfig(AppConfig): # Let the background worker check for migrations InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations) + self.update_site_url() self.collect_notification_methods() self.collect_state_transition_methods() @@ -223,6 +224,46 @@ class InvenTreeConfig(AppConfig): except Exception as 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): """Add a user on startup.""" # stop if checks were already created diff --git a/InvenTree/InvenTree/context.py b/InvenTree/InvenTree/context.py index e25a27d668..11bbb52e21 100644 --- a/InvenTree/InvenTree/context.py +++ b/InvenTree/InvenTree/context.py @@ -72,7 +72,7 @@ def user_roles(request): roles = {} - for role in RuleSet.RULESET_MODELS.keys(): + for role in RuleSet.get_ruleset_models().keys(): permissions = {} for perm in ['view', 'add', 'change', 'delete']: diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 3b5f15665b..3358804a9a 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -6,7 +6,6 @@ from urllib.parse import urlencode from django import forms from django.conf import settings from django.contrib.auth.models import Group, User -from django.contrib.sites.models import Site from django.http import HttpResponseRedirect from django.urls import reverse 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 rest_framework import serializers +import InvenTree.helpers_model import InvenTree.sso from common.models import InvenTreeSetting from InvenTree.exceptions import log_error @@ -293,7 +293,8 @@ class CustomUrlMixin: def get_email_confirmation_url(self, request, emailconfirmation): """Custom email confirmation (activation) url.""" 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( diff --git a/InvenTree/InvenTree/helpers_model.py b/InvenTree/InvenTree/helpers_model.py index b55e274f11..037dd17d32 100644 --- a/InvenTree/InvenTree/helpers_model.py +++ b/InvenTree/InvenTree/helpers_model.py @@ -34,47 +34,59 @@ def getSetting(key, backup_value=None): return common.models.InvenTreeSetting.get_setting(key, backup_value=backup_value) -def construct_absolute_url(*arg, **kwargs): +def get_base_url(request=None): + """Return the base URL for the InvenTree server. + + The base URL is determined in the following order of decreasing priority: + + 1. If a request object is provided, use the request URL + 2. Multi-site is enabled, and the current site has a valid URL + 3. If settings.SITE_URL is set (e.g. in the Django settings), use that + 4. If the InvenTree setting INVENTREE_BASE_URL is set, use that + """ + # Check if a request is provided + if request: + return request.build_absolute_uri('/') + + # Check if multi-site is enabled + try: + 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 + ): + return site_url + except (ProgrammingError, OperationalError): + pass + + # No base URL available + return '' + + +def construct_absolute_url(*arg, base_url=None, request=None): """Construct (or attempt to construct) an absolute URL from a relative URL. - This is useful when (for example) sending an email to a user with a link - to something in the InvenTree web framework. - A URL is constructed in the following order: - 1. If settings.SITE_URL is set (e.g. in the Django settings), use that - 2. If the InvenTree setting INVENTREE_BASE_URL is set, use that - 3. Otherwise, use the current request URL (if available) + 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 a site URL is provided, use that - site_url = getattr(settings, 'SITE_URL', None) + if not base_url: + base_url = get_base_url(request=request) - if not site_url: - # Otherwise, try to use the InvenTree setting - try: - site_url = common.models.InvenTreeSetting.get_setting( - 'INVENTREE_BASE_URL', create=False, cache=False - ) - except (ProgrammingError, OperationalError): - pass - - if not site_url: - # Otherwise, try to use the current request - 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): - """Return the base URL for the InvenTree server.""" - return construct_absolute_url('', **kwargs) + return urljoin(base_url, relative_url) def download_image_from_url(remote_url, timeout=2.5): diff --git a/InvenTree/InvenTree/magic_login.py b/InvenTree/InvenTree/magic_login.py index 725b913bbf..8d8df798ae 100644 --- a/InvenTree/InvenTree/magic_login.py +++ b/InvenTree/InvenTree/magic_login.py @@ -2,7 +2,6 @@ from django.conf import settings from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.core.mail import send_mail from django.template.loader import render_to_string from django.urls import reverse @@ -13,18 +12,20 @@ from rest_framework import serializers from rest_framework.response import Response from rest_framework.views import APIView +import InvenTree.version + def send_simple_login_email(user, link): """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( 'InvenTree/user_simple_login.txt', context ) send_mail( - _(f'[{site.name}] Log in to the app'), + _(f'[{site_name}] Log in to the app'), email_plaintext_message, settings.DEFAULT_FROM_EMAIL, [user.email], diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 4f13ec9fac..e88b9527fd 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -7,7 +7,6 @@ from decimal import Decimal from django.conf import settings from django.contrib.auth.models import User -from django.contrib.sites.models import Site from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -26,7 +25,7 @@ from taggit.serializers import TaggitSerializer import common.models as common_models from common.settings import currency_code_default, currency_code_mappings 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): @@ -445,19 +444,23 @@ class UserCreateSerializer(ExendedUserSerializer): def create(self, validated_data): """Send an e email to the user after creation.""" + base_url = get_base_url() + instance = super().create(validated_data) # Make sure the user cannot login until they have set a 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=_( - f'Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain}).' - ), + + message = _( + '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 diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 0eaae34726..73aefd2a75 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -978,13 +978,30 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Use database transactions when importing / exporting data 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 SOCIAL_BACKENDS = get_setting( 'INVENTREE_SOCIAL_BACKENDS', 'social_backends', [], typecast=list ) +if not SITE_MULTI: + INSTALLED_APPS.remove('django.contrib.sites') + for app in SOCIAL_BACKENDS: # Ensure that the app starts with 'allauth.socialaccount.providers' social_prefix = 'allauth.socialaccount.providers.' @@ -1096,16 +1113,6 @@ PLUGIN_RETRY = get_setting( ) # How often should plugin loading be tried? 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 CUSTOM_LOGO = get_custom_file( 'INVENTREE_CUSTOM_LOGO', 'customize.logo', 'custom logo', lookup_media=True diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index b58f53534c..0cda36703b 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -10,7 +10,6 @@ from unittest import mock import django.core.exceptions as django_exceptions from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.sites.models import Site from django.core import mail from django.core.exceptions import ValidationError from django.test import TestCase, override_settings @@ -372,7 +371,7 @@ class TestHelpers(TestCase): for url, expected in tests.items(): # Test with supplied base URL self.assertEqual( - InvenTree.helpers_model.construct_absolute_url(url, site_url=base), + InvenTree.helpers_model.construct_absolute_url(url, base_url=base), expected, ) @@ -1049,6 +1048,12 @@ class TestInstanceName(InvenTreeTestCase): 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 site_obj = Site.objects.all().order_by('id').first() self.assertEqual(site_obj.name, 'Testing title') @@ -1060,9 +1065,18 @@ class TestInstanceName(InvenTreeTestCase): '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 - site_obj = Site.objects.all().order_by('id').first() - self.assertEqual(site_obj.domain, 'http://127.1.2.3') + try: + from django.contrib.sites.models import Site + + site_obj = Site.objects.all().order_by('id').first() + self.assertEqual(site_obj.domain, 'http://127.1.2.3') + except Exception: + pass class TestOffloadTask(InvenTreeTestCase): @@ -1234,7 +1248,7 @@ class MagicLoginTest(InvenTreeTestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.data, {'status': 'ok'}) 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 self.assertTrue('http://testserver/api/email/login/' in mail.outbox[0].body) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8bb855084e..84d02cba26 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -24,7 +24,6 @@ from django.contrib.auth.models import Group, User from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.humanize.templatetags.humanize import naturaltime -from django.contrib.sites.models import Site from django.core.cache import cache from django.core.exceptions import AppRegistryNotReady, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator @@ -101,6 +100,10 @@ class BaseURLValidator(URLValidator): """Make sure empty values pass.""" 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: pass @@ -647,7 +650,7 @@ class BaseInvenTreeSetting(models.Model): return value @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. Args: @@ -1065,6 +1068,15 @@ def settings_group_options(): def update_instance_url(setting): """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.domain = setting.value site_obj.save() @@ -1072,6 +1084,15 @@ def update_instance_url(setting): def update_instance_name(setting): """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.name = setting.value site_obj.save() diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 9baef5fbce..6c1b1eadb5 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -90,8 +90,8 @@ language: en-us timezone: UTC # Base URL for the InvenTree server -# Use the environment variable INVENTREE_BASE_URL -# base_url: 'http://localhost:8000' +# Use the environment variable INVENTREE_SITE_URL +# site_url: 'http://localhost:8000' # Base currency code (or use env var INVENTREE_BASE_CURRENCY) base_currency: USD diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 7e5e348f77..fdc886adf1 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -201,160 +201,170 @@ class RuleSet(models.Model): RULESET_PERMISSIONS = ['view', 'add', 'change', 'delete'] - RULESET_MODELS = { - 'admin': [ - 'auth_group', - 'auth_user', - 'auth_permission', - 'users_apitoken', - 'users_ruleset', - 'report_reportasset', - 'report_reportsnippet', - 'report_billofmaterialsreport', - 'report_purchaseorderreport', - 'report_salesorderreport', - 'account_emailaddress', - 'account_emailconfirmation', - 'sites_site', - 'socialaccount_socialaccount', - 'socialaccount_socialapp', - 'socialaccount_socialtoken', - 'otp_totp_totpdevice', - 'otp_static_statictoken', - 'otp_static_staticdevice', - 'plugin_pluginconfig', - 'plugin_pluginsetting', - 'plugin_notificationusersetting', - 'common_newsfeedentry', - 'taggit_tag', - 'taggit_taggeditem', - 'flags_flagstate', - ], - 'part_category': [ - 'part_partcategory', - 'part_partcategoryparametertemplate', - 'part_partcategorystar', - ], - 'part': [ - 'part_part', - 'part_partpricing', - 'part_bomitem', - 'part_bomitemsubstitute', - 'part_partattachment', - 'part_partsellpricebreak', - 'part_partinternalpricebreak', - 'part_parttesttemplate', - 'part_partparametertemplate', - 'part_partparameter', - 'part_partrelated', - 'part_partstar', - 'part_partcategorystar', - 'company_supplierpart', - 'company_manufacturerpart', - 'company_manufacturerpartparameter', - 'company_manufacturerpartattachment', - 'label_partlabel', - ], - 'stocktake': ['part_partstocktake', 'part_partstocktakereport'], - 'stock_location': [ - 'stock_stocklocation', - 'stock_stocklocationtype', - 'label_stocklocationlabel', - 'report_stocklocationreport', - ], - 'stock': [ - 'stock_stockitem', - 'stock_stockitemattachment', - 'stock_stockitemtracking', - 'stock_stockitemtestresult', - 'report_testreport', - 'label_stockitemlabel', - ], - 'build': [ - 'part_part', - 'part_partcategory', - 'part_bomitem', - 'part_bomitemsubstitute', - 'build_build', - 'build_builditem', - 'build_buildline', - 'build_buildorderattachment', - 'stock_stockitem', - 'stock_stocklocation', - 'report_buildreport', - 'label_buildlinelabel', - ], - 'purchase_order': [ - 'company_company', - 'company_companyattachment', - 'company_contact', - 'company_address', - 'company_manufacturerpart', - 'company_manufacturerpartparameter', - 'company_supplierpart', - 'company_supplierpricebreak', - 'order_purchaseorder', - 'order_purchaseorderattachment', - 'order_purchaseorderlineitem', - 'order_purchaseorderextraline', - 'report_purchaseorderreport', - ], - 'sales_order': [ - 'company_company', - 'company_companyattachment', - 'company_contact', - 'company_address', - 'order_salesorder', - 'order_salesorderallocation', - 'order_salesorderattachment', - 'order_salesorderlineitem', - 'order_salesorderextraline', - 'order_salesordershipment', - 'report_salesorderreport', - ], - 'return_order': [ - 'company_company', - 'company_companyattachment', - 'company_contact', - 'company_address', - 'order_returnorder', - 'order_returnorderlineitem', - 'order_returnorderextraline', - 'order_returnorderattachment', - 'report_returnorderreport', - ], - } + @staticmethod + def get_ruleset_models(): + """Return a dictionary of models associated with each ruleset.""" + ruleset_models = { + 'admin': [ + 'auth_group', + 'auth_user', + 'auth_permission', + 'users_apitoken', + 'users_ruleset', + 'report_reportasset', + 'report_reportsnippet', + 'report_billofmaterialsreport', + 'report_purchaseorderreport', + 'report_salesorderreport', + 'account_emailaddress', + 'account_emailconfirmation', + 'socialaccount_socialaccount', + 'socialaccount_socialapp', + 'socialaccount_socialtoken', + 'otp_totp_totpdevice', + 'otp_static_statictoken', + 'otp_static_staticdevice', + 'plugin_pluginconfig', + 'plugin_pluginsetting', + 'plugin_notificationusersetting', + 'common_newsfeedentry', + 'taggit_tag', + 'taggit_taggeditem', + 'flags_flagstate', + ], + 'part_category': [ + 'part_partcategory', + 'part_partcategoryparametertemplate', + 'part_partcategorystar', + ], + 'part': [ + 'part_part', + 'part_partpricing', + 'part_bomitem', + 'part_bomitemsubstitute', + 'part_partattachment', + 'part_partsellpricebreak', + 'part_partinternalpricebreak', + 'part_parttesttemplate', + 'part_partparametertemplate', + 'part_partparameter', + 'part_partrelated', + 'part_partstar', + 'part_partcategorystar', + 'company_supplierpart', + 'company_manufacturerpart', + 'company_manufacturerpartparameter', + 'company_manufacturerpartattachment', + 'label_partlabel', + ], + 'stocktake': ['part_partstocktake', 'part_partstocktakereport'], + 'stock_location': [ + 'stock_stocklocation', + 'stock_stocklocationtype', + 'label_stocklocationlabel', + 'report_stocklocationreport', + ], + 'stock': [ + 'stock_stockitem', + 'stock_stockitemattachment', + 'stock_stockitemtracking', + 'stock_stockitemtestresult', + 'report_testreport', + 'label_stockitemlabel', + ], + 'build': [ + 'part_part', + 'part_partcategory', + 'part_bomitem', + 'part_bomitemsubstitute', + 'build_build', + 'build_builditem', + 'build_buildline', + 'build_buildorderattachment', + 'stock_stockitem', + 'stock_stocklocation', + 'report_buildreport', + 'label_buildlinelabel', + ], + 'purchase_order': [ + 'company_company', + 'company_companyattachment', + 'company_contact', + 'company_address', + 'company_manufacturerpart', + 'company_manufacturerpartparameter', + 'company_supplierpart', + 'company_supplierpricebreak', + 'order_purchaseorder', + 'order_purchaseorderattachment', + 'order_purchaseorderlineitem', + 'order_purchaseorderextraline', + 'report_purchaseorderreport', + ], + 'sales_order': [ + 'company_company', + 'company_companyattachment', + 'company_contact', + 'company_address', + 'order_salesorder', + 'order_salesorderallocation', + 'order_salesorderattachment', + 'order_salesorderlineitem', + 'order_salesorderextraline', + 'order_salesordershipment', + 'report_salesorderreport', + ], + 'return_order': [ + 'company_company', + 'company_companyattachment', + 'company_contact', + 'company_address', + 'order_returnorder', + 'order_returnorderlineitem', + 'order_returnorderextraline', + 'order_returnorderattachment', + 'report_returnorderreport', + ], + } + + if settings.SITE_MULTI: + ruleset_models['admin'].append('sites_site') + + return ruleset_models # Database models we ignore permission sets for - RULESET_IGNORE = [ - # Core django models (not user configurable) - 'admin_logentry', - 'contenttypes_contenttype', - # Models which currently do not require permissions - 'common_colortheme', - 'common_customunit', - 'common_inventreesetting', - 'common_inventreeusersetting', - 'common_notificationentry', - 'common_notificationmessage', - 'common_notesimage', - 'common_projectcode', - 'common_webhookendpoint', - 'common_webhookmessage', - 'label_labeloutput', - 'users_owner', - # Third-party tables - 'error_report_error', - 'exchange_rate', - 'exchange_exchangebackend', - 'user_sessions_session', - # Django-q - 'django_q_ormq', - 'django_q_failure', - 'django_q_task', - 'django_q_schedule', - 'django_q_success', - ] + @staticmethod + def get_ruleset_ignore(): + """Return a list of database tables which do not require permissions.""" + return [ + # Core django models (not user configurable) + 'admin_logentry', + 'contenttypes_contenttype', + # Models which currently do not require permissions + 'common_colortheme', + 'common_customunit', + 'common_inventreesetting', + 'common_inventreeusersetting', + 'common_notificationentry', + 'common_notificationmessage', + 'common_notesimage', + 'common_projectcode', + 'common_webhookendpoint', + 'common_webhookmessage', + 'label_labeloutput', + 'users_owner', + # Third-party tables + 'error_report_error', + 'exchange_rate', + 'exchange_exchangebackend', + 'user_sessions_session', + # Django-q + 'django_q_ormq', + 'django_q_failure', + 'django_q_task', + 'django_q_schedule', + 'django_q_success', + ] RULESET_CHANGE_INHERIT = [('part', 'partparameter'), ('part', 'bomitem')] @@ -409,12 +419,12 @@ class RuleSet(models.Model): return True # If the table does *not* require permissions - if table in cls.RULESET_IGNORE: + if table in cls.get_ruleset_ignore(): return True # Work out which roles touch the given table 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): return True @@ -474,7 +484,7 @@ class RuleSet(models.Model): def get_models(self): """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): @@ -669,7 +679,7 @@ def clear_user_role_cache(user): Args: 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']: key = f'role_{user}_{role}_{perm}' cache.delete(key) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index 54d56759f7..4e29bf0497 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -14,7 +14,7 @@ class RuleSetModelTest(TestCase): def test_ruleset_models(self): """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 @@ -30,16 +30,16 @@ class RuleSetModelTest(TestCase): if len(extra) > 0: # pragma: no cover 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: print('-', e) # 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 - 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: print('-', e) @@ -62,8 +62,8 @@ class RuleSetModelTest(TestCase): assigned_models = set() # Now check that each defined model is a valid table name - for key in RuleSet.RULESET_MODELS.keys(): - models = RuleSet.RULESET_MODELS[key] + for key in RuleSet.get_ruleset_models().keys(): + models = RuleSet.get_ruleset_models()[key] for m in models: assigned_models.add(m) @@ -72,7 +72,8 @@ class RuleSetModelTest(TestCase): for model in available_tables: 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 missing_models.add(model) @@ -90,7 +91,7 @@ class RuleSetModelTest(TestCase): for model in assigned_models: defined_models.add(model) - for model in RuleSet.RULESET_IGNORE: + for model in RuleSet.get_ruleset_ignore(): defined_models.add(model) for model in defined_models: # pragma: no cover @@ -118,7 +119,7 @@ class RuleSetModelTest(TestCase): # Check that all permissions have been assigned permissions? permission_set = set() - for models in RuleSet.RULESET_MODELS.values(): + for models in RuleSet.get_ruleset_models().values(): for model in models: permission_set.add(model) diff --git a/docs/docs/assets/images/settings/social_application_configure.png b/docs/docs/assets/images/settings/social_application_configure.png index 52565e84cf..6eeb026761 100644 Binary files a/docs/docs/assets/images/settings/social_application_configure.png and b/docs/docs/assets/images/settings/social_application_configure.png differ diff --git a/docs/docs/start/advanced.md b/docs/docs/start/advanced.md index 1064e318de..6bcc40873a 100644 --- a/docs/docs/start/advanced.md +++ b/docs/docs/start/advanced.md @@ -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.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. | + +## 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* | diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index da4b68dea5..cc944f2ed3 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -55,6 +55,7 @@ The following basic options are available: | INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING | | INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False | | 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_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin | | INVENTREE_LANGUAGE | language | Default language | en-us |