4
4
import structlog
5
5
from django .conf import settings
6
6
7
- from readthedocs .builds .constants import EXTERNAL
8
- from readthedocs .core .utils .extend import SettingsOverrideObject
7
+ from readthedocs .builds .constants import EXTERNAL , INTERNAL
9
8
from readthedocs .core .utils .url import unsafe_join_url_path
10
9
from readthedocs .subscriptions .constants import TYPE_CNAME
11
10
from readthedocs .subscriptions .products import get_feature
12
11
13
12
log = structlog .get_logger (__name__ )
14
13
15
14
16
- class ResolverBase :
15
+ class Resolver :
17
16
18
17
"""
19
18
Read the Docs URL Resolver.
@@ -56,7 +55,6 @@ class ResolverBase:
56
55
57
56
def base_resolve_path (
58
57
self ,
59
- project_slug ,
60
58
filename ,
61
59
version_slug = None ,
62
60
language = None ,
@@ -91,7 +89,6 @@ def base_resolve_path(
91
89
92
90
subproject_alias = project_relationship .alias if project_relationship else ""
93
91
return path .format (
94
- project = project_slug ,
95
92
filename = filename ,
96
93
version = version_slug ,
97
94
language = language ,
@@ -112,7 +109,7 @@ def resolve_path(
112
109
113
110
filename = self ._fix_filename (filename )
114
111
115
- parent_project , project_relationship = self ._get_canonical_project_data (project )
112
+ parent_project , project_relationship = self ._get_canonical_project (project )
116
113
single_version = bool (project .single_version or single_version )
117
114
118
115
# If the project is a subproject, we use the custom prefix
@@ -125,7 +122,6 @@ def resolve_path(
125
122
custom_prefix = parent_project .custom_prefix
126
123
127
124
return self .base_resolve_path (
128
- project_slug = parent_project .slug ,
129
125
filename = filename ,
130
126
version_slug = version_slug ,
131
127
language = language ,
@@ -134,25 +130,94 @@ def resolve_path(
134
130
custom_prefix = custom_prefix ,
135
131
)
136
132
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
+ ):
138
175
"""
139
176
Get the domain from where the documentation of ``project`` is served from.
140
177
141
178
:param project: Project object
142
179
:param bool use_canonical_domain: If `True` use its canonical custom domain if available.
180
+ :returns: Tuple of ``(domain, use_https)``.
143
181
"""
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
149
194
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
151
217
152
218
def resolve (
153
219
self ,
154
220
project ,
155
- require_https = False ,
156
221
filename = "" ,
157
222
query_params = "" ,
158
223
external = None ,
@@ -165,36 +230,11 @@ def resolve(
165
230
if external is None :
166
231
external = self ._is_external (project , version_slug )
167
232
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 ,
195
236
)
196
- protocol = "https" if use_https_protocol else "http"
197
-
237
+ protocol = "https" if use_https else "http"
198
238
path = self .resolve_path (project , filename = filename , ** kwargs )
199
239
return urlunparse ((protocol , domain , path , "" , query_params , "" ))
200
240
@@ -211,26 +251,14 @@ def get_subproject_url_prefix(self, project, external_version_slug=None):
211
251
:param project: Project object to get the root URL from
212
252
:param external_version_slug: If given, resolve using the external version domain.
213
253
"""
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
+ )
229
257
protocol = "https" if use_https else "http"
230
258
path = project .subproject_prefix
231
259
return urlunparse ((protocol , domain , path , "" , "" , "" ))
232
260
233
- def _get_canonical_project_data (self , project ):
261
+ def _get_canonical_project (self , project ):
234
262
"""
235
263
Get the parent project and subproject relationship from the canonical project of `project`.
236
264
@@ -287,38 +315,7 @@ def _get_canonical_project_data(self, project):
287
315
if relationship :
288
316
parent_project = relationship .parent
289
317
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
322
319
323
320
def _get_external_subdomain (self , project , version_slug ):
324
321
"""Determine domain for an external version."""
@@ -351,28 +348,12 @@ def _fix_filename(self, filename):
351
348
filename = filename .lstrip ("/" )
352
349
return filename
353
350
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
-
365
351
def _use_cname (self , project ):
366
352
"""Test if to allow direct serving for project on CNAME."""
367
353
return bool (get_feature (project , feature_type = TYPE_CNAME ))
368
354
369
355
370
- class Resolver (SettingsOverrideObject ):
371
- _default_class = ResolverBase
372
- _override_setting = "RESOLVER_CLASS"
373
-
374
-
375
356
resolver = Resolver ()
376
357
resolve_path = resolver .resolve_path
377
- resolve_domain = resolver .resolve_domain
358
+ resolve_domain = resolver .get_domain_without_protocol
378
359
resolve = resolver .resolve
0 commit comments