Implement optional url prefix the Django way

* Before, the optional url prefix feature required calling our
  versioning version of reverse(). This worked _ok_ until we added more
  and more urls from 3rd party apps. Those 3rd party apps do not call
  our reverse(), writefully so.
* This implementation looks at the incoming request path. If it includes
  the special optional prefix url, then we register ALL the urls WITH
  the optional url prefix.
  If the incoming request path does NOT contain the options url prefix
  then we register ALL the urls WITHOUT the optional url prefix.
* Before this, we were registering BOTH sets of urls and then reverse()
  + the request as context to decide which url.
This commit is contained in:
Chris Meyers 2024-04-08 15:58:14 -04:00 committed by Chris Meyers
parent 61ec03e540
commit 0645d342dd
4 changed files with 59 additions and 28 deletions

View File

@ -29,9 +29,7 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
kwargs = {}
if 'version' not in kwargs:
kwargs['version'] = settings.REST_FRAMEWORK['DEFAULT_VERSION']
url = drf_reverse(viewname, args, kwargs, request, format, **extra)
return transform_optional_api_urlpattern_prefix_url(request, url)
return drf_reverse(viewname, args, kwargs, request, format, **extra)
class URLPathVersioning(BaseVersioning):

View File

@ -1,6 +1,7 @@
# Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved.
import functools
import logging
import threading
import time
@ -18,6 +19,7 @@ from django.urls import reverse, resolve
from awx.main import migrations
from awx.main.utils.profiling import AWXProfiler
from awx.main.utils.common import memoize
from awx.urls import get_urlpatterns
logger = logging.getLogger('awx.main.middleware')
@ -173,3 +175,27 @@ class MigrationRanCheckMiddleware(MiddlewareMixin):
def process_request(self, request):
if is_migrating() and getattr(resolve(request.path), 'url_name', '') != 'migrations_notran':
return redirect(reverse("ui:migrations_notran"))
class OptionalURLPrefixPath(MiddlewareMixin):
@functools.lru_cache
def _url_optional(self, prefix):
# Relavant Django code path https://github.com/django/django/blob/stable/4.2.x/django/core/handlers/base.py#L300
#
# resolve_request(request)
# get_resolver(request.urlconf)
# _get_cached_resolver(request.urlconf) <-- cached via @functools.cache
#
# Django will attempt to cache the value(s) of request.urlconf
# Being hashable is a prerequisit for being cachable.
# tuple() is hashable list() is not.
# Hence the tuple(list()) wrap.
return tuple(get_urlpatterns(prefix=prefix))
def process_request(self, request):
prefix = settings.OPTIONAL_API_URLPATTERN_PREFIX
if request.path.startswith(f"/api/{prefix}"):
request.urlconf = self._url_optional(prefix)
else:
request.urlconf = 'awx.urls'

View File

@ -1002,6 +1002,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'awx.main.middleware.DisableLocalAuthMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'awx.main.middleware.OptionalURLPrefixPath',
'awx.sso.middleware.SocialAuthMiddleware',
'crum.CurrentRequestUserMiddleware',
'awx.main.middleware.URLModificationMiddleware',

View File

@ -9,36 +9,42 @@ from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
from awx.main.views import handle_400, handle_403, handle_404, handle_500, handle_csp_violation, handle_login_redirect
urlpatterns = [
re_path(r'', include('awx.ui.urls', namespace='ui')),
re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')),
path('api/', include('awx.api.urls', namespace='api')),
]
def get_urlpatterns(prefix=None):
if not prefix:
prefix = '/'
else:
prefix = f'/{prefix}/'
if settings.OPTIONAL_API_URLPATTERN_PREFIX:
urlpatterns += [
path(f'api/{settings.OPTIONAL_API_URLPATTERN_PREFIX}/', include('awx.api.urls')),
urlpatterns = [
re_path(r'', include('awx.ui.urls', namespace='ui')),
re_path(r'^ui_next/.*', include('awx.ui_next.urls', namespace='ui_next')),
path(f'api{prefix}', include('awx.api.urls', namespace='api')),
]
urlpatterns += [
re_path(r'^api/v2/', include(resource_api_urls)),
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
re_path(r'^sso/', include('social_django.urls', namespace='social')),
re_path(r'^(?:api/)?400.html$', handle_400),
re_path(r'^(?:api/)?403.html$', handle_403),
re_path(r'^(?:api/)?404.html$', handle_404),
re_path(r'^(?:api/)?500.html$', handle_500),
re_path(r'^csp-violation/', handle_csp_violation),
re_path(r'^login/', handle_login_redirect),
]
urlpatterns += [
path(f'api{prefix}v2/', include(resource_api_urls)),
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
re_path(r'^sso/', include('social_django.urls', namespace='social')),
re_path(r'^(?:api/)?400.html$', handle_400),
re_path(r'^(?:api/)?403.html$', handle_403),
re_path(r'^(?:api/)?404.html$', handle_404),
re_path(r'^(?:api/)?500.html$', handle_500),
re_path(r'^csp-violation/', handle_csp_violation),
re_path(r'^login/', handle_login_redirect),
]
if settings.SETTINGS_MODULE == 'awx.settings.development':
try:
import debug_toolbar
if settings.SETTINGS_MODULE == 'awx.settings.development':
try:
import debug_toolbar
urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))]
except ImportError:
pass
urlpatterns += [re_path(r'^__debug__/', include(debug_toolbar.urls))]
except ImportError:
pass
return urlpatterns
urlpatterns = get_urlpatterns()
handler400 = 'awx.main.views.handle_400'
handler403 = 'awx.main.views.handle_403'