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:
parent
538ff9be7b
commit
5bc00298c6
@ -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
|
||||||
|
@ -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']:
|
||||||
|
@ -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(
|
||||||
|
@ -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):
|
||||||
|
"""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.
|
"""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
|
Args:
|
||||||
to something in the InvenTree web framework.
|
*arg: The relative URL to construct
|
||||||
A URL is constructed in the following order:
|
base_url: The base URL to use for the construction (if not provided, will attempt to determine from settings)
|
||||||
1. If settings.SITE_URL is set (e.g. in the Django settings), use that
|
request: The request object to use for the construction (optional)
|
||||||
2. If the InvenTree setting INVENTREE_BASE_URL is set, use that
|
|
||||||
3. Otherwise, use the current request URL (if available)
|
|
||||||
"""
|
"""
|
||||||
relative_url = '/'.join(arg)
|
relative_url = '/'.join(arg)
|
||||||
|
|
||||||
# If a site URL is provided, use that
|
if not base_url:
|
||||||
site_url = getattr(settings, 'SITE_URL', None)
|
base_url = get_base_url(request=request)
|
||||||
|
|
||||||
if not site_url:
|
return urljoin(base_url, relative_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)
|
|
||||||
|
|
||||||
|
|
||||||
def download_image_from_url(remote_url, timeout=2.5):
|
def download_image_from_url(remote_url, timeout=2.5):
|
||||||
|
@ -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],
|
||||||
|
@ -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()
|
message = _(
|
||||||
domain = current_site.domain
|
'Your account has been created.\n\nPlease use the password reset function to login'
|
||||||
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}).'
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
site_obj = Site.objects.all().order_by('id').first()
|
try:
|
||||||
self.assertEqual(site_obj.domain, 'http://127.1.2.3')
|
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):
|
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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -201,160 +201,170 @@ class RuleSet(models.Model):
|
|||||||
|
|
||||||
RULESET_PERMISSIONS = ['view', 'add', 'change', 'delete']
|
RULESET_PERMISSIONS = ['view', 'add', 'change', 'delete']
|
||||||
|
|
||||||
RULESET_MODELS = {
|
@staticmethod
|
||||||
'admin': [
|
def get_ruleset_models():
|
||||||
'auth_group',
|
"""Return a dictionary of models associated with each ruleset."""
|
||||||
'auth_user',
|
ruleset_models = {
|
||||||
'auth_permission',
|
'admin': [
|
||||||
'users_apitoken',
|
'auth_group',
|
||||||
'users_ruleset',
|
'auth_user',
|
||||||
'report_reportasset',
|
'auth_permission',
|
||||||
'report_reportsnippet',
|
'users_apitoken',
|
||||||
'report_billofmaterialsreport',
|
'users_ruleset',
|
||||||
'report_purchaseorderreport',
|
'report_reportasset',
|
||||||
'report_salesorderreport',
|
'report_reportsnippet',
|
||||||
'account_emailaddress',
|
'report_billofmaterialsreport',
|
||||||
'account_emailconfirmation',
|
'report_purchaseorderreport',
|
||||||
'sites_site',
|
'report_salesorderreport',
|
||||||
'socialaccount_socialaccount',
|
'account_emailaddress',
|
||||||
'socialaccount_socialapp',
|
'account_emailconfirmation',
|
||||||
'socialaccount_socialtoken',
|
'socialaccount_socialaccount',
|
||||||
'otp_totp_totpdevice',
|
'socialaccount_socialapp',
|
||||||
'otp_static_statictoken',
|
'socialaccount_socialtoken',
|
||||||
'otp_static_staticdevice',
|
'otp_totp_totpdevice',
|
||||||
'plugin_pluginconfig',
|
'otp_static_statictoken',
|
||||||
'plugin_pluginsetting',
|
'otp_static_staticdevice',
|
||||||
'plugin_notificationusersetting',
|
'plugin_pluginconfig',
|
||||||
'common_newsfeedentry',
|
'plugin_pluginsetting',
|
||||||
'taggit_tag',
|
'plugin_notificationusersetting',
|
||||||
'taggit_taggeditem',
|
'common_newsfeedentry',
|
||||||
'flags_flagstate',
|
'taggit_tag',
|
||||||
],
|
'taggit_taggeditem',
|
||||||
'part_category': [
|
'flags_flagstate',
|
||||||
'part_partcategory',
|
],
|
||||||
'part_partcategoryparametertemplate',
|
'part_category': [
|
||||||
'part_partcategorystar',
|
'part_partcategory',
|
||||||
],
|
'part_partcategoryparametertemplate',
|
||||||
'part': [
|
'part_partcategorystar',
|
||||||
'part_part',
|
],
|
||||||
'part_partpricing',
|
'part': [
|
||||||
'part_bomitem',
|
'part_part',
|
||||||
'part_bomitemsubstitute',
|
'part_partpricing',
|
||||||
'part_partattachment',
|
'part_bomitem',
|
||||||
'part_partsellpricebreak',
|
'part_bomitemsubstitute',
|
||||||
'part_partinternalpricebreak',
|
'part_partattachment',
|
||||||
'part_parttesttemplate',
|
'part_partsellpricebreak',
|
||||||
'part_partparametertemplate',
|
'part_partinternalpricebreak',
|
||||||
'part_partparameter',
|
'part_parttesttemplate',
|
||||||
'part_partrelated',
|
'part_partparametertemplate',
|
||||||
'part_partstar',
|
'part_partparameter',
|
||||||
'part_partcategorystar',
|
'part_partrelated',
|
||||||
'company_supplierpart',
|
'part_partstar',
|
||||||
'company_manufacturerpart',
|
'part_partcategorystar',
|
||||||
'company_manufacturerpartparameter',
|
'company_supplierpart',
|
||||||
'company_manufacturerpartattachment',
|
'company_manufacturerpart',
|
||||||
'label_partlabel',
|
'company_manufacturerpartparameter',
|
||||||
],
|
'company_manufacturerpartattachment',
|
||||||
'stocktake': ['part_partstocktake', 'part_partstocktakereport'],
|
'label_partlabel',
|
||||||
'stock_location': [
|
],
|
||||||
'stock_stocklocation',
|
'stocktake': ['part_partstocktake', 'part_partstocktakereport'],
|
||||||
'stock_stocklocationtype',
|
'stock_location': [
|
||||||
'label_stocklocationlabel',
|
'stock_stocklocation',
|
||||||
'report_stocklocationreport',
|
'stock_stocklocationtype',
|
||||||
],
|
'label_stocklocationlabel',
|
||||||
'stock': [
|
'report_stocklocationreport',
|
||||||
'stock_stockitem',
|
],
|
||||||
'stock_stockitemattachment',
|
'stock': [
|
||||||
'stock_stockitemtracking',
|
'stock_stockitem',
|
||||||
'stock_stockitemtestresult',
|
'stock_stockitemattachment',
|
||||||
'report_testreport',
|
'stock_stockitemtracking',
|
||||||
'label_stockitemlabel',
|
'stock_stockitemtestresult',
|
||||||
],
|
'report_testreport',
|
||||||
'build': [
|
'label_stockitemlabel',
|
||||||
'part_part',
|
],
|
||||||
'part_partcategory',
|
'build': [
|
||||||
'part_bomitem',
|
'part_part',
|
||||||
'part_bomitemsubstitute',
|
'part_partcategory',
|
||||||
'build_build',
|
'part_bomitem',
|
||||||
'build_builditem',
|
'part_bomitemsubstitute',
|
||||||
'build_buildline',
|
'build_build',
|
||||||
'build_buildorderattachment',
|
'build_builditem',
|
||||||
'stock_stockitem',
|
'build_buildline',
|
||||||
'stock_stocklocation',
|
'build_buildorderattachment',
|
||||||
'report_buildreport',
|
'stock_stockitem',
|
||||||
'label_buildlinelabel',
|
'stock_stocklocation',
|
||||||
],
|
'report_buildreport',
|
||||||
'purchase_order': [
|
'label_buildlinelabel',
|
||||||
'company_company',
|
],
|
||||||
'company_companyattachment',
|
'purchase_order': [
|
||||||
'company_contact',
|
'company_company',
|
||||||
'company_address',
|
'company_companyattachment',
|
||||||
'company_manufacturerpart',
|
'company_contact',
|
||||||
'company_manufacturerpartparameter',
|
'company_address',
|
||||||
'company_supplierpart',
|
'company_manufacturerpart',
|
||||||
'company_supplierpricebreak',
|
'company_manufacturerpartparameter',
|
||||||
'order_purchaseorder',
|
'company_supplierpart',
|
||||||
'order_purchaseorderattachment',
|
'company_supplierpricebreak',
|
||||||
'order_purchaseorderlineitem',
|
'order_purchaseorder',
|
||||||
'order_purchaseorderextraline',
|
'order_purchaseorderattachment',
|
||||||
'report_purchaseorderreport',
|
'order_purchaseorderlineitem',
|
||||||
],
|
'order_purchaseorderextraline',
|
||||||
'sales_order': [
|
'report_purchaseorderreport',
|
||||||
'company_company',
|
],
|
||||||
'company_companyattachment',
|
'sales_order': [
|
||||||
'company_contact',
|
'company_company',
|
||||||
'company_address',
|
'company_companyattachment',
|
||||||
'order_salesorder',
|
'company_contact',
|
||||||
'order_salesorderallocation',
|
'company_address',
|
||||||
'order_salesorderattachment',
|
'order_salesorder',
|
||||||
'order_salesorderlineitem',
|
'order_salesorderallocation',
|
||||||
'order_salesorderextraline',
|
'order_salesorderattachment',
|
||||||
'order_salesordershipment',
|
'order_salesorderlineitem',
|
||||||
'report_salesorderreport',
|
'order_salesorderextraline',
|
||||||
],
|
'order_salesordershipment',
|
||||||
'return_order': [
|
'report_salesorderreport',
|
||||||
'company_company',
|
],
|
||||||
'company_companyattachment',
|
'return_order': [
|
||||||
'company_contact',
|
'company_company',
|
||||||
'company_address',
|
'company_companyattachment',
|
||||||
'order_returnorder',
|
'company_contact',
|
||||||
'order_returnorderlineitem',
|
'company_address',
|
||||||
'order_returnorderextraline',
|
'order_returnorder',
|
||||||
'order_returnorderattachment',
|
'order_returnorderlineitem',
|
||||||
'report_returnorderreport',
|
'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
|
# Database models we ignore permission sets for
|
||||||
RULESET_IGNORE = [
|
@staticmethod
|
||||||
# Core django models (not user configurable)
|
def get_ruleset_ignore():
|
||||||
'admin_logentry',
|
"""Return a list of database tables which do not require permissions."""
|
||||||
'contenttypes_contenttype',
|
return [
|
||||||
# Models which currently do not require permissions
|
# Core django models (not user configurable)
|
||||||
'common_colortheme',
|
'admin_logentry',
|
||||||
'common_customunit',
|
'contenttypes_contenttype',
|
||||||
'common_inventreesetting',
|
# Models which currently do not require permissions
|
||||||
'common_inventreeusersetting',
|
'common_colortheme',
|
||||||
'common_notificationentry',
|
'common_customunit',
|
||||||
'common_notificationmessage',
|
'common_inventreesetting',
|
||||||
'common_notesimage',
|
'common_inventreeusersetting',
|
||||||
'common_projectcode',
|
'common_notificationentry',
|
||||||
'common_webhookendpoint',
|
'common_notificationmessage',
|
||||||
'common_webhookmessage',
|
'common_notesimage',
|
||||||
'label_labeloutput',
|
'common_projectcode',
|
||||||
'users_owner',
|
'common_webhookendpoint',
|
||||||
# Third-party tables
|
'common_webhookmessage',
|
||||||
'error_report_error',
|
'label_labeloutput',
|
||||||
'exchange_rate',
|
'users_owner',
|
||||||
'exchange_exchangebackend',
|
# Third-party tables
|
||||||
'user_sessions_session',
|
'error_report_error',
|
||||||
# Django-q
|
'exchange_rate',
|
||||||
'django_q_ormq',
|
'exchange_exchangebackend',
|
||||||
'django_q_failure',
|
'user_sessions_session',
|
||||||
'django_q_task',
|
# Django-q
|
||||||
'django_q_schedule',
|
'django_q_ormq',
|
||||||
'django_q_success',
|
'django_q_failure',
|
||||||
]
|
'django_q_task',
|
||||||
|
'django_q_schedule',
|
||||||
|
'django_q_success',
|
||||||
|
]
|
||||||
|
|
||||||
RULESET_CHANGE_INHERIT = [('part', 'partparameter'), ('part', 'bomitem')]
|
RULESET_CHANGE_INHERIT = [('part', 'partparameter'), ('part', 'bomitem')]
|
||||||
|
|
||||||
@ -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)
|
||||||
|
@ -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 |
@ -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* |
|
||||||
|
@ -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 |
|
||||||
|
Loading…
x
Reference in New Issue
Block a user