From 7d87b8b8966325616e19a916034c842c82b7125d Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Apr 2025 08:27:41 +1000 Subject: [PATCH] [UI] Add CUI compatibility URLs (#9541) * Add CUI compatibility URLs * Add config option to enable URL compatibility * Add unit tests * Simplify tests --- src/backend/InvenTree/InvenTree/config.py | 9 ++ src/backend/InvenTree/InvenTree/tests.py | 19 ++++ src/backend/InvenTree/InvenTree/urls.py | 5 + src/backend/InvenTree/web/urls.py | 123 +++++++++++++++++++++- 4 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/config.py b/src/backend/InvenTree/InvenTree/config.py index 234f940a05..02326f0e90 100644 --- a/src/backend/InvenTree/InvenTree/config.py +++ b/src/backend/InvenTree/InvenTree/config.py @@ -484,4 +484,13 @@ def get_frontend_settings(debug=True): # If no servers are specified, show server selector frontend_settings['show_server_selector'] = True + # Support compatibility with "legacy" URLs? + try: + frontend_settings['url_compatibility'] = bool( + frontend_settings.get('url_compatibility', True) + ) + except Exception: + # If the value is not a boolean, set it to True + frontend_settings['url_compatibility'] = True + return frontend_settings diff --git a/src/backend/InvenTree/InvenTree/tests.py b/src/backend/InvenTree/InvenTree/tests.py index 85f2ca2802..5f3275d14d 100644 --- a/src/backend/InvenTree/InvenTree/tests.py +++ b/src/backend/InvenTree/InvenTree/tests.py @@ -1704,3 +1704,22 @@ class SchemaPostprocessingTest(TestCase): self.assertNotIn('customer_detail', schemas_out.get('SalesOrder')['required']) # required key removed when empty self.assertNotIn('required', schemas_out.get('SalesOrderShipment')) + + +class URLCompatibilityTest(InvenTreeTestCase): + """Unit test for legacy URL compatibility.""" + + URL_MAPPINGS = [ + ('/index/', '/web'), + ('/part/1/', '/web/part/1/'), + ('/company/customers/', '/web/sales/index/customers'), + ('/build/3/', '/web/manufacturing/build-order/3'), + ('/stock/item/1/', '/web/stock/item/1/'), + ] + + def test_legacy_urls(self): + """Test legacy URLs.""" + for old_url, new_url in self.URL_MAPPINGS: + response = self.client.get(old_url) + self.assertEqual(response.status_code, 302) + self.assertEqual(response['Location'], new_url) diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index c79867fd3e..e0a603d17f 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -28,6 +28,7 @@ import report.api import stock.api import users.api from plugin.urls import get_plugin_urls +from web.urls import cui_compatibility_urls from web.urls import urlpatterns as platform_urls from .api import ( @@ -172,6 +173,10 @@ urlpatterns.append( ) ) +# Compatibility layer for old (CUI) URLs +if settings.FRONTEND_SETTINGS.get('url_compatibility'): + urlpatterns += cui_compatibility_urls(settings.FRONTEND_URL_BASE) + # Send any unknown URLs to the index page urlpatterns += [ re_path( diff --git a/src/backend/InvenTree/web/urls.py b/src/backend/InvenTree/web/urls.py index 2827fa257e..b55b4937dd 100644 --- a/src/backend/InvenTree/web/urls.py +++ b/src/backend/InvenTree/web/urls.py @@ -3,10 +3,131 @@ from django.conf import settings from django.urls import include, path, re_path from django.views.decorators.csrf import ensure_csrf_cookie -from django.views.generic import TemplateView +from django.views.generic import RedirectView, TemplateView spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name='web/index.html')) + +def cui_compatibility_urls(base: str) -> list: + """Generate a list of URL patterns for compatibility with old (CUI) URLs. + + These URLs are provided for backwards compatibility with older versions of InvenTree, + before we moved to a SPA (Single Page Application) architecture in the 1.0.0 release. + + At some future point these may be removed? + + Args: + base (str): The base URL to use for generating the patterns. + + Returns: + list: A list of URL patterns. + """ + return [ + # Old 'index' view - reroute to the dashboard + path('index/', RedirectView.as_view(url=f'/{base}')), + path('settings/', RedirectView.as_view(url=f'/{base}/settings')), + # Company patterns + path( + 'company/', + include([ + path( + 'customers/', + RedirectView.as_view(url=f'/{base}/sales/index/customers'), + ), + path( + 'manufacturers/', + RedirectView.as_view(url=f'/{base}/purchasing/index/manufacturers'), + ), + path( + 'suppliers/', + RedirectView.as_view(url=f'/{base}/purchasing/index/suppliers'), + ), + re_path( + r'(?P\d+)/', RedirectView.as_view(url=f'/{base}/company/%(pk)s') + ), + ]), + ), + # "Part" app views + re_path( + r'^part/(?P.*)$', RedirectView.as_view(url=f'/{base}/part/%(path)s') + ), + # "Stock" app views + re_path( + r'^stock/(?P.*)$', RedirectView.as_view(url=f'/{base}/stock/%(path)s') + ), + # "Build" app views (requires some custom handling) + path( + 'build/', + include([ + re_path( + r'^(?P\d+)/', + RedirectView.as_view( + url=f'/{base}/manufacturing/build-order/%(pk)s' + ), + ), + re_path('.*', RedirectView.as_view(url=f'/{base}/manufacturing/')), + ]), + ), + # "Order" app views + path( + 'order/', + include([ + path( + 'purchase-order/', + include([ + re_path( + r'^(?P\d+)/', + RedirectView.as_view( + url=f'/{base}/purchasing/purchase-order/%(pk)s' + ), + ), + re_path( + '.*', + RedirectView.as_view( + url=f'/{base}/purchasing/index/purchaseorders/' + ), + ), + ]), + ), + path( + 'sales-order/', + include([ + re_path( + r'^(?P\d+)/', + RedirectView.as_view( + url=f'/{base}/sales/sales-order/%(pk)s' + ), + ), + re_path( + '.*', + RedirectView.as_view( + url=f'/{base}/sales/index/salesorders/' + ), + ), + ]), + ), + path( + 'return-order/', + include([ + re_path( + r'^(?P\d+)/', + RedirectView.as_view( + url=f'/{base}/sales/return-order/%(pk)s' + ), + ), + re_path( + '.*', + RedirectView.as_view( + url=f'/{base}/sales/index/returnorders/' + ), + ), + ]), + ), + ]), + ), + ] + + urlpatterns = [ path( f'{settings.FRONTEND_URL_BASE}/',