Skip to content

URLConf: fix support for subprojects #8327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions readthedocs/core/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,19 @@ def base_resolve_path(
'$filename',
'{filename}',
)
url = url.replace(
'$subproject',
'{subproject_slug}',
)
# Remove the subproject from the path if
# we are resolving the main project.
# /{subproject}/foo/bar -> /foo/bar.
if subproject_slug:
url = url.replace(
'$subproject',
'{subproject_slug}',
)
else:
url = url.replace(
'$subproject/',
'',
)
if '$' in url:
log.warning(
'Unconverted variable in a resolver URLConf: url=%s', url
Expand Down
43 changes: 31 additions & 12 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,12 +606,13 @@ def proxied_api_host(self):
This needs to start with a slash at the root of the domain,
and end without a slash
"""
url = '_'
if self.urlconf:
# Add our proxied api host at the first place we have a $variable
# This supports both subpaths & normal root hosting
url_prefix = self.urlconf.split('$', 1)[0]
return '/' + url_prefix.strip('/') + '/_'
return '/_'
url = url_prefix.strip('/') + '/_'
return '/' + url.strip('/')

@property
def proxied_api_url(self):
Expand All @@ -628,33 +629,43 @@ def regex_urlconf(self):
Convert User's URLConf into a proper django URLConf.

This replaces the user-facing syntax with the regex syntax.

:returns: A tuple with the urlconf for the main project and subprojects.
"""
to_convert = re.escape(self.urlconf)

subproject_urlconf = to_convert
main_urlconf = to_convert.replace('\\$subproject\\/', '')
if self.single_version:
main_urlconf = main_urlconf.replace('\\$version\\/', '')
main_urlconf = main_urlconf.replace('\\$language\\/', '')
return self._to_regex_urlconf(main_urlconf), self._to_regex_urlconf(subproject_urlconf)

def _to_regex_urlconf(self, urlconf):
# We should standardize these names so we can loop over them easier
to_convert = to_convert.replace(
urlconf = urlconf.replace(
'\\$version',
'(?P<version_slug>{regex})'.format(regex=pattern_opts['version_slug'])
)
to_convert = to_convert.replace(
urlconf = urlconf.replace(
'\\$language',
'(?P<lang_slug>{regex})'.format(regex=pattern_opts['lang_slug'])
)
to_convert = to_convert.replace(
urlconf = urlconf.replace(
'\\$filename',
'(?P<filename>{regex})'.format(regex=pattern_opts['filename_slug'])
)
to_convert = to_convert.replace(
urlconf = urlconf.replace(
'\\$subproject',
'(?P<subproject_slug>{regex})'.format(regex=pattern_opts['project_slug'])
)

if '\\$' in to_convert:
if '\\$' in urlconf:
log.warning(
'Unconverted variable in a project URLConf: project=%s to_convert=%s',
self, to_convert
'Unconverted variable in a project URLConf: project=%s urlconf=%s',
self, urlconf
)
return to_convert
return urlconf

@property
def proxito_urlconf(self):
Expand All @@ -664,9 +675,9 @@ def proxito_urlconf(self):
It is used for doc serving on projects that have their own ``urlconf``.
"""
from readthedocs.projects.views.public import ProjectDownloadMedia
from readthedocs.proxito.urls import core_urls
from readthedocs.proxito.views.serve import ServeDocs
from readthedocs.proxito.views.utils import proxito_404_page_handler
from readthedocs.proxito.urls import core_urls

class ProxitoURLConf:

Expand All @@ -691,12 +702,20 @@ class ProxitoURLConf:
name='user_proxied_downloads'
),
]

main_urlconf, subproject_urlconf = self.regex_urlconf
docs_urls = [
re_path(
'^{regex_urlconf}$'.format(regex_urlconf=self.regex_urlconf),
'^{regex_urlconf}$'.format(regex_urlconf=subproject_urlconf),
ServeDocs.as_view(),
name='user_proxied_serve_docs_subprojects'
),
re_path(
'^{regex_urlconf}$'.format(regex_urlconf=main_urlconf),
ServeDocs.as_view(),
name='user_proxied_serve_docs'
),

# paths for redirects at the root
re_path(
'^{proxied_api_url}$'.format(
Expand Down
91 changes: 91 additions & 0 deletions readthedocs/proxito/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,94 @@ def test_middleware_urlconf_subproject(self):
resp['X-Accel-Redirect'],
'/proxito/media/html/subproject/testing/foodex.html',
)

# The main project still works.
resp = self.client.get('/subpath/latest/en/foo.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo.html',
)

resp = self.client.get('/subpath/latest/en/foo/bar/index.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo/bar/index.html',
)

def test_serve_subprojects_from_root(self):
self.pip.urlconf = '$subproject/$language/$version/$filename'
self.pip.save()
resp = self.client.get('/subproject/en/testing/foodex.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/subproject/testing/foodex.html',
)

# The main project still works.
resp = self.client.get('/en/latest/foo.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo.html',
)

resp = self.client.get('/en/latest/foo/bar/index.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo/bar/index.html',
)

def test_middleware_urlconf_subproject_main_project_single_version(self):
self.pip.single_version = True
self.pip.save()
resp = self.client.get('/subpath/subproject/testing/en/foodex.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/subproject/testing/foodex.html',
)

# The main project still works.
resp = self.client.get('/subpath/foo.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo.html',
)

resp = self.client.get('/subpath/foo/bar/index.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo/bar/index.html',
)

def test_serve_subprojects_from_root_main_project_single_version(self):
self.pip.urlconf = '$subproject/$language/$version/$filename'
self.pip.single_version = True
self.pip.save()
resp = self.client.get('/subproject/en/testing/foodex.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/subproject/testing/foodex.html',
)

# The main project still works.
resp = self.client.get('/foo.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo.html',
)

resp = self.client.get('/foo/bar/index.html', HTTP_HOST=self.domain)
self.assertEqual(resp.status_code, 200)
self.assertEqual(
resp['X-Accel-Redirect'],
'/proxito/media/html/pip/latest/foo/bar/index.html',
)