Skip to content

Commit 1ac3b46

Browse files
authored
Resolver: refactor (#10813)
Simplify a couple of things, and we can remove the overrides from .com after this. - _get_canonical_project and _get_canonical_project_data are basically the same, the former tries to recursively resolve the project for cases that we don't really support, and the second just checks for the cases that we do support and returns the relationship in case of a subproject. So I just renamed _get_canonical_project_data to _get_canonical_project. - We were passing the project slug when resolving the path (this was a residual from where we were allowing `USE_SUBDOMAIN=False`). - Resolving is now split into two steps: resolving the domain, and resolving the path. - We were using `require_https` for .com only, this was since on .com we were using the https attribute to track the progress of a custom domain, this is no longer the case, all custom domains on .com are https. - `_use_custom_domain` is the same as `_use_cname`. - Two more methods to resolve a path were added, they are basically the same as `resolve`, but they work on the object itself, instead of passing each part separately. This results in fewer queries in case the version object is already in memory.
1 parent 90f1791 commit 1ac3b46

File tree

2 files changed

+187
-130
lines changed

2 files changed

+187
-130
lines changed

readthedocs/core/resolver.py

+90-109
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
import structlog
55
from django.conf import settings
66

7-
from readthedocs.builds.constants import EXTERNAL
8-
from readthedocs.core.utils.extend import SettingsOverrideObject
7+
from readthedocs.builds.constants import EXTERNAL, INTERNAL
98
from readthedocs.core.utils.url import unsafe_join_url_path
109
from readthedocs.subscriptions.constants import TYPE_CNAME
1110
from readthedocs.subscriptions.products import get_feature
1211

1312
log = structlog.get_logger(__name__)
1413

1514

16-
class ResolverBase:
15+
class Resolver:
1716

1817
"""
1918
Read the Docs URL Resolver.
@@ -56,7 +55,6 @@ class ResolverBase:
5655

5756
def base_resolve_path(
5857
self,
59-
project_slug,
6058
filename,
6159
version_slug=None,
6260
language=None,
@@ -91,7 +89,6 @@ def base_resolve_path(
9189

9290
subproject_alias = project_relationship.alias if project_relationship else ""
9391
return path.format(
94-
project=project_slug,
9592
filename=filename,
9693
version=version_slug,
9794
language=language,
@@ -112,7 +109,7 @@ def resolve_path(
112109

113110
filename = self._fix_filename(filename)
114111

115-
parent_project, project_relationship = self._get_canonical_project_data(project)
112+
parent_project, project_relationship = self._get_canonical_project(project)
116113
single_version = bool(project.single_version or single_version)
117114

118115
# If the project is a subproject, we use the custom prefix
@@ -125,7 +122,6 @@ def resolve_path(
125122
custom_prefix = parent_project.custom_prefix
126123

127124
return self.base_resolve_path(
128-
project_slug=parent_project.slug,
129125
filename=filename,
130126
version_slug=version_slug,
131127
language=language,
@@ -134,25 +130,94 @@ def resolve_path(
134130
custom_prefix=custom_prefix,
135131
)
136132

137-
def resolve_domain(self, project, use_canonical_domain=True):
133+
def resolve_version(self, project, version=None, filename="/"):
134+
"""
135+
Get the URL for a specific version of a project.
136+
137+
If no version is given, the default version is used.
138+
139+
Use this instead of ``resolve`` if you have the version object already.
140+
"""
141+
if not version:
142+
default_version_slug = project.get_default_version()
143+
version = project.versions(manager=INTERNAL).get(slug=default_version_slug)
144+
145+
domain, use_https = self._get_project_domain(
146+
project,
147+
external_version_slug=version.slug if version.is_external else None,
148+
)
149+
path = self.resolve_path(
150+
project=project,
151+
filename=filename,
152+
version_slug=version.slug,
153+
language=project.language,
154+
single_version=project.single_version,
155+
)
156+
protocol = "https" if use_https else "http"
157+
return urlunparse((protocol, domain, path, "", "", ""))
158+
159+
def resolve_project(self, project, filename="/"):
160+
"""
161+
Get the URL for a project.
162+
163+
This is the URL where the project is served from,
164+
it doesn't include the version or language.
165+
166+
Useful to link to a known filename in the project.
167+
"""
168+
domain, use_https = self._get_project_domain(project)
169+
protocol = "https" if use_https else "http"
170+
return urlunparse((protocol, domain, filename, "", "", ""))
171+
172+
def _get_project_domain(
173+
self, project, external_version_slug=None, use_canonical_domain=True
174+
):
138175
"""
139176
Get the domain from where the documentation of ``project`` is served from.
140177
141178
:param project: Project object
142179
:param bool use_canonical_domain: If `True` use its canonical custom domain if available.
180+
:returns: Tuple of ``(domain, use_https)``.
143181
"""
144-
canonical_project = self._get_canonical_project(project)
145-
if use_canonical_domain and self._use_cname(canonical_project):
146-
domain = canonical_project.get_canonical_custom_domain()
147-
if domain:
148-
return domain.domain
182+
use_https = settings.PUBLIC_DOMAIN_USES_HTTPS
183+
canonical_project, _ = self._get_canonical_project(project)
184+
domain = self._get_project_subdomain(canonical_project)
185+
if external_version_slug:
186+
domain = self._get_external_subdomain(
187+
canonical_project, external_version_slug
188+
)
189+
elif use_canonical_domain and self._use_cname(canonical_project):
190+
domain_object = canonical_project.get_canonical_custom_domain()
191+
if domain_object:
192+
use_https = domain_object.https
193+
domain = domain_object.domain
149194

150-
return self._get_project_subdomain(canonical_project)
195+
return domain, use_https
196+
197+
def get_domain(self, project, use_canonical_domain=True):
198+
domain, use_https = self._get_project_domain(
199+
project, use_canonical_domain=use_canonical_domain
200+
)
201+
protocol = "https" if use_https else "http"
202+
return urlunparse((protocol, domain, "", "", "", ""))
203+
204+
def get_domain_without_protocol(self, project, use_canonical_domain=True):
205+
"""
206+
Get the domain from where the documentation of ``project`` is served from.
207+
208+
This doesn't include the protocol.
209+
210+
:param project: Project object
211+
:param bool use_canonical_domain: If `True` use its canonical custom domain if available.
212+
"""
213+
domain, _ = self._get_project_domain(
214+
project, use_canonical_domain=use_canonical_domain
215+
)
216+
return domain
151217

152218
def resolve(
153219
self,
154220
project,
155-
require_https=False,
156221
filename="",
157222
query_params="",
158223
external=None,
@@ -165,36 +230,11 @@ def resolve(
165230
if external is None:
166231
external = self._is_external(project, version_slug)
167232

168-
canonical_project = self._get_canonical_project(project)
169-
custom_domain = canonical_project.get_canonical_custom_domain()
170-
use_custom_domain = self._use_custom_domain(custom_domain)
171-
172-
if external:
173-
domain = self._get_external_subdomain(canonical_project, version_slug)
174-
elif use_custom_domain:
175-
domain = custom_domain.domain
176-
else:
177-
domain = self._get_project_subdomain(canonical_project)
178-
179-
use_https_protocol = any(
180-
[
181-
# Rely on the ``Domain.https`` field
182-
use_custom_domain and custom_domain.https,
183-
# or force it if specified
184-
require_https,
185-
# or fallback to settings
186-
settings.PUBLIC_DOMAIN_USES_HTTPS
187-
and settings.PUBLIC_DOMAIN
188-
and any(
189-
[
190-
settings.PUBLIC_DOMAIN in domain,
191-
settings.RTD_EXTERNAL_VERSION_DOMAIN in domain,
192-
]
193-
),
194-
]
233+
domain, use_https = self._get_project_domain(
234+
project,
235+
external_version_slug=version_slug if external else None,
195236
)
196-
protocol = "https" if use_https_protocol else "http"
197-
237+
protocol = "https" if use_https else "http"
198238
path = self.resolve_path(project, filename=filename, **kwargs)
199239
return urlunparse((protocol, domain, path, "", query_params, ""))
200240

@@ -211,26 +251,14 @@ def get_subproject_url_prefix(self, project, external_version_slug=None):
211251
:param project: Project object to get the root URL from
212252
:param external_version_slug: If given, resolve using the external version domain.
213253
"""
214-
canonical_project = self._get_canonical_project(project)
215-
use_custom_domain = self._use_cname(canonical_project)
216-
custom_domain = canonical_project.get_canonical_custom_domain()
217-
if external_version_slug:
218-
domain = self._get_external_subdomain(
219-
canonical_project, external_version_slug
220-
)
221-
use_https = settings.PUBLIC_DOMAIN_USES_HTTPS
222-
elif use_custom_domain and custom_domain:
223-
domain = custom_domain.domain
224-
use_https = custom_domain.https
225-
else:
226-
domain = self._get_project_subdomain(canonical_project)
227-
use_https = settings.PUBLIC_DOMAIN_USES_HTTPS
228-
254+
domain, use_https = self._get_project_domain(
255+
project, external_version_slug=external_version_slug
256+
)
229257
protocol = "https" if use_https else "http"
230258
path = project.subproject_prefix
231259
return urlunparse((protocol, domain, path, "", "", ""))
232260

233-
def _get_canonical_project_data(self, project):
261+
def _get_canonical_project(self, project):
234262
"""
235263
Get the parent project and subproject relationship from the canonical project of `project`.
236264
@@ -287,38 +315,7 @@ def _get_canonical_project_data(self, project):
287315
if relationship:
288316
parent_project = relationship.parent
289317

290-
return (parent_project, relationship)
291-
292-
def _get_canonical_project(self, project, projects=None):
293-
"""
294-
Recursively get canonical project for subproject or translations.
295-
296-
We need to recursively search here as a nested translations inside
297-
subprojects, and vice versa, are supported.
298-
299-
:type project: Project
300-
:type projects: List of projects for iteration
301-
:rtype: Project
302-
"""
303-
# Track what projects have already been traversed to avoid infinite
304-
# recursion. We can't determine a root project well here, so you get
305-
# what you get if you have configured your project in a strange manner
306-
if projects is None:
307-
projects = {project}
308-
else:
309-
projects.add(project)
310-
311-
next_project = None
312-
if project.main_language_project:
313-
next_project = project.main_language_project
314-
else:
315-
relation = project.parent_relationship
316-
if relation:
317-
next_project = relation.parent
318-
319-
if next_project and next_project not in projects:
320-
return self._get_canonical_project(next_project, projects)
321-
return project
318+
return parent_project, relationship
322319

323320
def _get_external_subdomain(self, project, version_slug):
324321
"""Determine domain for an external version."""
@@ -351,28 +348,12 @@ def _fix_filename(self, filename):
351348
filename = filename.lstrip("/")
352349
return filename
353350

354-
def _use_custom_domain(self, custom_domain):
355-
"""
356-
Make decision about whether to use a custom domain to serve docs.
357-
358-
Always use the custom domain if it exists.
359-
360-
:param custom_domain: Domain instance or ``None``
361-
:type custom_domain: readthedocs.projects.models.Domain
362-
"""
363-
return custom_domain is not None
364-
365351
def _use_cname(self, project):
366352
"""Test if to allow direct serving for project on CNAME."""
367353
return bool(get_feature(project, feature_type=TYPE_CNAME))
368354

369355

370-
class Resolver(SettingsOverrideObject):
371-
_default_class = ResolverBase
372-
_override_setting = "RESOLVER_CLASS"
373-
374-
375356
resolver = Resolver()
376357
resolve_path = resolver.resolve_path
377-
resolve_domain = resolver.resolve_domain
358+
resolve_domain = resolver.get_domain_without_protocol
378359
resolve = resolver.resolve

0 commit comments

Comments
 (0)