diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py
index 310309a890..72e81f04d2 100644
--- a/InvenTree/InvenTree/config.py
+++ b/InvenTree/InvenTree/config.py
@@ -7,6 +7,7 @@ import os
import random
import shutil
import string
+import warnings
from pathlib import Path
logger = logging.getLogger('inventree')
@@ -341,3 +342,58 @@ def get_custom_file(env_ref: str, conf_ref: str, log_ref: str, lookup_media: boo
value = False
return value
+
+
+def get_frontend_settings(debug=True):
+ """Return a dictionary of settings for the frontend interface.
+
+ Note that the new config settings use the 'FRONTEND' key,
+ whereas the legacy key was 'PUI' (platform UI) which is now deprecated
+ """
+
+ # Legacy settings
+ pui_settings = get_setting('INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict)
+
+ if len(pui_settings) > 0:
+ warnings.warn(
+ "The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead",
+ DeprecationWarning, stacklevel=2
+ )
+
+ # New settings
+ frontend_settings = get_setting('INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict)
+
+ # Merge settings
+ settings = {**pui_settings, **frontend_settings}
+
+ # Set the base URL
+ if 'base_url' not in settings:
+ base_url = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', '')
+
+ if base_url:
+ warnings.warn(
+ "The 'INVENTREE_PUI_URL_BASE' key is deprecated. Please use 'INVENTREE_FRONTEND_URL_BASE' instead",
+ DeprecationWarning, stacklevel=2
+ )
+ else:
+ base_url = get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform')
+
+ settings['base_url'] = base_url
+
+ # Set the server list
+ settings['server_list'] = settings.get('server_list', [])
+
+ # Set the debug flag
+ settings['debug'] = debug
+
+ if 'environment' not in settings:
+ settings['environment'] = 'development' if debug else 'production'
+
+ if debug and 'show_server_selector' not in settings:
+ # In debug mode, show server selector by default
+ settings['show_server_selector'] = True
+ elif len(settings['server_list']) == 0:
+ # If no servers are specified, show server selector
+ settings['show_server_selector'] = True
+
+ return settings
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
index c34c5416e8..7fac82e13f 100644
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object):
elif request.path_info.startswith('/accounts/'):
authorized = True
- elif request.path_info.startswith(f'/{settings.PUI_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.PUI_URL_BASE}':
+ elif request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.FRONTEND_URL_BASE}':
authorized = True
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 206b4a697b..a808f6d603 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -1044,9 +1044,9 @@ CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', '
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
-# Frontend settings
-PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform')
-PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {})
+# Load settings for the frontend interface
+FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
+FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")
@@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS:
# Magic login django-sesame
SESAME_MAX_AGE = 300
-# LOGIN_REDIRECT_URL = f"/{PUI_URL_BASE}/logged-in/"
+# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
LOGIN_REDIRECT_URL = "/index/"
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 5687d5f7a6..584f69a246 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase):
self.assertEqual(resp.url, '/index/')
# Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet
# TODO: In the future, the URL comparison will need to be reverted
- # self.assertEqual(resp.url, f'/{settings.PUI_URL_BASE}/logged-in/')
+ # self.assertEqual(resp.url, f'/{settings.FRONTEND_URL_BASE}/logged-in/')
# And we should be logged in again
self.assertEqual(resp.wsgi_request.user, self.user)
diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml
index 5755949cac..1e51863a68 100644
--- a/InvenTree/config_template.yaml
+++ b/InvenTree/config_template.yaml
@@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER
# logo: img/custom_logo.png
# splash: img/custom_splash.jpg
-# Platform UI options
-# pui_settings:
+# Frontend UI settings
+# frontend_settings:
+# base_url: 'frontend'
# server_list:
# my_server1:
-# host: https://demo.inventree.org/api/
+# host: https://demo.inventree.org/
# name: InvenTree Demo
# default_server: my_server1
# show_server_selector: false
# sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
# environment: development
-# Base URL for serving Platform UI
-# pui_url_base: 'platform'
# Custom flags
# InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py
index 7554158b72..95d59a6e82 100644
--- a/InvenTree/users/admin.py
+++ b/InvenTree/users/admin.py
@@ -6,7 +6,6 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
-from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from users.models import ApiToken, Owner, RuleSet
@@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
users = form.cleaned_data['users']
# Check for users who are members of multiple groups
- warning_message = ''
+ multiple_group_users = []
+
for user in users:
if user.groups.all().count() > 1:
- warning_message += f'
- {user.username} is member of: '
- for idx, group in enumerate(user.groups.all()):
- warning_message += f'{group.name}'
- if idx < len(user.groups.all()) - 1:
- warning_message += ', '
+ multiple_group_users.append(user.username)
# If any, display warning message when group is saved
- if warning_message:
- warning_message = mark_safe(_(f'The following users are members of multiple groups:'
- f'{warning_message}'))
- messages.add_message(request, messages.WARNING, warning_message)
+ if len(multiple_group_users) > 0:
+
+ msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users)
+
+ messages.add_message(
+ request,
+ messages.WARNING,
+ msg
+ )
def save_formset(self, request, form, formset, change):
"""Save the inline formset"""
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index cce30ecc91..803ed1c614 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -422,11 +422,7 @@ class RuleSet(models.Model):
"""Construct the correctly formatted permission string, given the app_model name, and the permission type."""
model, app = split_model(model)
- return "{app}.{perm}_{model}".format(
- app=app,
- perm=permission,
- model=model
- )
+ return f"{app}.{permission}_{model}"
def __str__(self, debug=False): # pragma: no cover
"""Ruleset string representation."""
@@ -504,12 +500,7 @@ def update_group_roles(group, debug=False):
# and create a simplified permission key string
for p in group.permissions.all().prefetch_related('content_type'):
(permission, app, model) = p.natural_key()
-
- permission_string = '{app}.{perm}'.format(
- app=app,
- perm=permission
- )
-
+ permission_string = f"{app}.{permission}"
group_permissions.add(permission_string)
# List of permissions which must be added to the group
@@ -527,7 +518,7 @@ def update_group_roles(group, debug=False):
allowed: Whether or not the action is allowed
"""
if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover
- raise ValueError("Action {a} is invalid".format(a=action))
+ raise ValueError(f"Action {action} is invalid")
permission_string = RuleSet.get_model_permission_string(model, action)
diff --git a/InvenTree/web/templatetags/spa_helper.py b/InvenTree/web/templatetags/spa_helper.py
index 4d8ebd9ae7..edea064d89 100644
--- a/InvenTree/web/templatetags/spa_helper.py
+++ b/InvenTree/web/templatetags/spa_helper.py
@@ -1,4 +1,5 @@
"""Template tag to render SPA imports."""
+
import json
from logging import getLogger
from pathlib import Path
@@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe
logger = getLogger("InvenTree")
register = template.Library()
-PUI_DEFAULTS = {
- 'url_base': settings.PUI_URL_BASE,
-}
-PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {}))
-PUI_SETTINGS = json.dumps(PUI_DEFAULTS)
+FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
@register.simple_tag
@@ -30,11 +27,11 @@ def spa_bundle():
index = manifest_data.get("index.html")
css_index = manifest_data.get("index.css")
- dynmanic_files = index.get("dynamicImports", [])
+ dynamic_files = index.get("dynamicImports", [])
imports_files = "".join(
[
f''
- for file in dynmanic_files
+ for file in dynamic_files
]
)
@@ -47,4 +44,4 @@ def spa_bundle():
@register.simple_tag
def spa_settings():
"""Render settings for spa."""
- return mark_safe(f"""""")
+ return mark_safe(f"""""")
diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py
index d1e409eeab..35726815ac 100644
--- a/InvenTree/web/urls.py
+++ b/InvenTree/web/urls.py
@@ -20,12 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html
urlpatterns = [
- path(f'{settings.PUI_URL_BASE}/', include([
+ path(f'{settings.FRONTEND_URL_BASE}/', include([
path("assets/", RedirectAssetView.as_view()),
re_path(r"^(?P.*)/$", spa_view),
path("set-password?uid=&token=", spa_view, name="password_reset_confirm"),
path("", spa_view),]
)),
- path(settings.PUI_URL_BASE, spa_view, name='platform'),
+ path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
path("assets/", RedirectAssetView.as_view()),
]
diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx
index 9f7a3657f8..c9477de5e6 100644
--- a/src/frontend/src/functions/auth.tsx
+++ b/src/frontend/src/functions/auth.tsx
@@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => {
return true;
};
+/**
+ * Logout the user (invalidate auth token)
+ */
export const doClassicLogout = async () => {
// TODO @matmair - logout from the server session
// Set token in context
diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx
index 48afa14def..e0e7fd44bb 100644
--- a/src/frontend/src/main.tsx
+++ b/src/frontend/src/main.tsx
@@ -14,7 +14,7 @@ declare global {
server_list: HostList;
default_server: string;
show_server_selector: boolean;
- url_base: string;
+ base_url: string;
sentry_dsn?: string;
environment?: string;
};
@@ -27,20 +27,20 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
window.INVENTREE_SETTINGS = {
server_list: {
- 'mantine-cqj63coxn': {
+ localhost: {
host: `${window.location.origin}/`,
name: 'Current Server'
},
...(IS_DEV_OR_DEMO
? {
- 'mantine-u56l5jt85': {
+ demo: {
host: 'https://demo.inventree.org/',
name: 'InvenTree Demo'
}
}
: {})
},
- default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', // use demo server for demo mode
+ default_server: IS_DEMO ? 'demo' : 'localhost',
show_server_selector: IS_DEV_OR_DEMO,
// merge in settings that are already set via django's spa_view or for development
@@ -56,7 +56,7 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) {
});
}
-export const url_base = window.INVENTREE_SETTINGS.url_base || 'platform';
+export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
@@ -66,5 +66,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
// Redirect to base url if on /
if (window.location.pathname === '/') {
- window.location.replace(`/${url_base}`);
+ window.location.replace(`/${base_url}`);
}
diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx
index 379343bc61..d704f4bf2b 100644
--- a/src/frontend/src/views/DesktopAppView.tsx
+++ b/src/frontend/src/views/DesktopAppView.tsx
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
import { queryClient, setApiDefaults } from '../App';
import { BaseContext } from '../contexts/BaseContext';
import { defaultHostList } from '../defaults/defaultHostList';
-import { url_base } from '../main';
+import { base_url } from '../main';
import { routes } from '../router';
import { useLocalState } from '../states/LocalState';
import { useSessionState } from '../states/SessionState';
@@ -49,7 +49,7 @@ export default function DesktopAppView() {
return (
- {routes}
+ {routes}
);