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} );