Merge ~cjwatson/launchpad:distribution-traversal into launchpad:master
- Git
- lp:~cjwatson/launchpad
- distribution-traversal
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 6f7cc0336b53708625d61934a86cf46f919f4856 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:distribution-traversal |
Merge into: | launchpad:master |
Diff against target: |
428 lines (+215/-12) 10 files modified
lib/lp/registry/browser/configure.zcml (+1/-2) lib/lp/registry/browser/distribution.py (+23/-5) lib/lp/registry/browser/distroseries.py (+36/-2) lib/lp/registry/browser/tests/distribution-views.txt (+2/-1) lib/lp/registry/browser/tests/test_distribution.py (+84/-0) lib/lp/registry/configure.zcml (+2/-0) lib/lp/registry/enums.py (+18/-1) lib/lp/registry/interfaces/distribution.py (+18/-1) lib/lp/registry/model/distribution.py (+5/-0) lib/lp/registry/tests/test_distribution.py (+26/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+393731@code.launchpad.net |
Commit message
Add configurable distribution default traversal rules
Description of the change
We can use redirect_
DB MP: https:/
To post a comment you must log in.
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
2 | index a718640..777652f 100644 |
3 | --- a/lib/lp/registry/browser/configure.zcml |
4 | +++ b/lib/lp/registry/browser/configure.zcml |
5 | @@ -99,8 +99,7 @@ |
6 | /> |
7 | <browser:url |
8 | for="lp.registry.interfaces.distroseries.IDistroSeries" |
9 | - path_expression="name" |
10 | - attribute_to_parent="distribution" |
11 | + urldata="lp.registry.browser.distroseries.DistroSeriesURL" |
12 | /> |
13 | <browser:defaultView |
14 | name="+index" |
15 | diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py |
16 | index 8d566e0..04d6dc1 100644 |
17 | --- a/lib/lp/registry/browser/distribution.py |
18 | +++ b/lib/lp/registry/browser/distribution.py |
19 | @@ -95,6 +95,7 @@ from lp.registry.browser.pillar import ( |
20 | PillarNavigationMixin, |
21 | PillarViewMixin, |
22 | ) |
23 | +from lp.registry.enums import DistributionDefaultTraversalPolicy |
24 | from lp.registry.interfaces.distribution import ( |
25 | IDistribution, |
26 | IDistributionMirrorMenuMarker, |
27 | @@ -189,13 +190,28 @@ class DistributionNavigation( |
28 | |
29 | @stepthrough('+series') |
30 | def traverse_series(self, name): |
31 | - series, _ = self._resolveSeries(name) |
32 | - return self.redirectSubTree( |
33 | - canonical_url(series, request=self.request), status=303) |
34 | + series, redirect = self._resolveSeries(name) |
35 | + if not redirect: |
36 | + policy = self.context.default_traversal_policy |
37 | + if (policy == DistributionDefaultTraversalPolicy.SERIES and |
38 | + not self.context.redirect_default_traversal): |
39 | + redirect = True |
40 | + if redirect: |
41 | + return self.redirectSubTree( |
42 | + canonical_url(series, request=self.request), status=303) |
43 | + else: |
44 | + return series |
45 | |
46 | def traverse(self, name): |
47 | - series, is_alias = self._resolveSeries(name) |
48 | - if is_alias: |
49 | + series, redirect = self._resolveSeries(name) |
50 | + if series is None: |
51 | + return None |
52 | + if not redirect: |
53 | + policy = self.context.default_traversal_policy |
54 | + if (policy == DistributionDefaultTraversalPolicy.SERIES and |
55 | + self.context.redirect_default_traversal): |
56 | + redirect = True |
57 | + if redirect: |
58 | return self.redirectSubTree( |
59 | canonical_url(series, request=self.request), status=303) |
60 | else: |
61 | @@ -965,6 +981,8 @@ class DistributionEditView(RegistryEditFormView, |
62 | 'translations_usage', |
63 | 'answers_usage', |
64 | 'translation_focus', |
65 | + 'default_traversal_policy', |
66 | + 'redirect_default_traversal', |
67 | ] |
68 | |
69 | custom_widget_icon = CustomWidgetFactory( |
70 | diff --git a/lib/lp/registry/browser/distroseries.py b/lib/lp/registry/browser/distroseries.py |
71 | index 89114b9..75c0468 100644 |
72 | --- a/lib/lp/registry/browser/distroseries.py |
73 | +++ b/lib/lp/registry/browser/distroseries.py |
74 | @@ -1,4 +1,4 @@ |
75 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
76 | +# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
77 | # GNU Affero General Public License version 3 (see the file LICENSE). |
78 | |
79 | """View classes related to `IDistroSeries`.""" |
80 | @@ -17,6 +17,7 @@ __all__ = [ |
81 | 'DistroSeriesPackageSearchView', |
82 | 'DistroSeriesPackagesView', |
83 | 'DistroSeriesUniquePackagesView', |
84 | + 'DistroSeriesURL', |
85 | 'DistroSeriesView', |
86 | ] |
87 | |
88 | @@ -27,7 +28,10 @@ from zope.component import getUtility |
89 | from zope.event import notify |
90 | from zope.formlib import form |
91 | from zope.formlib.widget import CustomWidgetFactory |
92 | -from zope.interface import Interface |
93 | +from zope.interface import ( |
94 | + implementer, |
95 | + Interface, |
96 | + ) |
97 | from zope.lifecycleevent import ObjectCreatedEvent |
98 | from zope.schema import ( |
99 | Choice, |
100 | @@ -67,6 +71,7 @@ from lp.registry.browser import ( |
101 | MilestoneOverlayMixin, |
102 | ) |
103 | from lp.registry.enums import ( |
104 | + DistributionDefaultTraversalPolicy, |
105 | DistroSeriesDifferenceStatus, |
106 | DistroSeriesDifferenceType, |
107 | ) |
108 | @@ -86,6 +91,7 @@ from lp.services.webapp.authorization import check_permission |
109 | from lp.services.webapp.batching import BatchNavigator |
110 | from lp.services.webapp.breadcrumb import Breadcrumb |
111 | from lp.services.webapp.escaping import structured |
112 | +from lp.services.webapp.interfaces import ICanonicalUrlData |
113 | from lp.services.webapp.menu import ( |
114 | ApplicationMenu, |
115 | enabled_with_permission, |
116 | @@ -129,6 +135,34 @@ def get_dsd_source(): |
117 | return getUtility(IDistroSeriesDifferenceSource) |
118 | |
119 | |
120 | +@implementer(ICanonicalUrlData) |
121 | +class DistroSeriesURL: |
122 | + """Distro series URL creation rules. |
123 | + |
124 | + The canonical URL for a distro series depends on the values of |
125 | + `default_traversal_policy` and `redirect_default_traversal` on the |
126 | + context distribution. |
127 | + """ |
128 | + |
129 | + rootsite = None |
130 | + |
131 | + def __init__(self, context): |
132 | + self.context = context |
133 | + |
134 | + @property |
135 | + def inside(self): |
136 | + return self.context.distribution |
137 | + |
138 | + @property |
139 | + def path(self): |
140 | + policy = self.context.distribution.default_traversal_policy |
141 | + if (policy == DistributionDefaultTraversalPolicy.SERIES and |
142 | + not self.context.distribution.redirect_default_traversal): |
143 | + return self.context.name |
144 | + else: |
145 | + return u"+series/%s" % self.context.name |
146 | + |
147 | + |
148 | class DistroSeriesNavigation(GetitemNavigation, BugTargetTraversalMixin, |
149 | StructuralSubscriptionTargetTraversalMixin): |
150 | |
151 | diff --git a/lib/lp/registry/browser/tests/distribution-views.txt b/lib/lp/registry/browser/tests/distribution-views.txt |
152 | index 810f118..533d559 100644 |
153 | --- a/lib/lp/registry/browser/tests/distribution-views.txt |
154 | +++ b/lib/lp/registry/browser/tests/distribution-views.txt |
155 | @@ -132,7 +132,8 @@ The view accepts most of the distribution fields. |
156 | 'package_derivatives_email', 'icon', |
157 | 'logo', 'mugshot', 'official_malone', 'enable_bug_expiration', |
158 | 'blueprints_usage', 'translations_usage', 'answers_usage', |
159 | - 'translation_focus'] |
160 | + 'translation_focus', |
161 | + 'default_traversal_policy', 'redirect_default_traversal'] |
162 | |
163 | >>> del form['field.name'] |
164 | >>> del form['field.actions.save'] |
165 | diff --git a/lib/lp/registry/browser/tests/test_distribution.py b/lib/lp/registry/browser/tests/test_distribution.py |
166 | index d47eb99..b92d7b2 100644 |
167 | --- a/lib/lp/registry/browser/tests/test_distribution.py |
168 | +++ b/lib/lp/registry/browser/tests/test_distribution.py |
169 | @@ -72,6 +72,42 @@ class TestDistributionNavigation(TestCaseWithFactory): |
170 | "http://launchpad.test/%s/%s" % ( |
171 | distroseries.distribution.name, distroseries.name)) |
172 | |
173 | + def test_classic_series_url_redirects(self): |
174 | + distroseries = self.factory.makeDistroSeries() |
175 | + distroseries.distribution.redirect_default_traversal = True |
176 | + self.assertRedirects( |
177 | + "http://launchpad.test/%s/%s" % ( |
178 | + distroseries.distribution.name, distroseries.name), |
179 | + "http://launchpad.test/%s/+series/%s" % ( |
180 | + distroseries.distribution.name, distroseries.name)) |
181 | + |
182 | + def test_classic_series_url_with_alias_redirects(self): |
183 | + distroseries = self.factory.makeDistroSeries() |
184 | + distroseries.distribution.redirect_default_traversal = True |
185 | + distroseries.distribution.development_series_alias = "devel" |
186 | + self.assertRedirects( |
187 | + "http://launchpad.test/%s/devel" % distroseries.distribution.name, |
188 | + "http://launchpad.test/%s/+series/%s" % ( |
189 | + distroseries.distribution.name, distroseries.name)) |
190 | + |
191 | + def test_new_series_url(self): |
192 | + distroseries = self.factory.makeDistroSeries() |
193 | + distroseries.distribution.redirect_default_traversal = True |
194 | + obj, _, _ = test_traverse( |
195 | + "http://launchpad.test/%s/+series/%s" % ( |
196 | + distroseries.distribution.name, distroseries.name)) |
197 | + self.assertEqual(distroseries, obj) |
198 | + |
199 | + def test_new_series_url_with_alias(self): |
200 | + distroseries = self.factory.makeDistroSeries() |
201 | + distroseries.distribution.redirect_default_traversal = True |
202 | + distroseries.distribution.development_series_alias = "devel" |
203 | + self.assertRedirects( |
204 | + "http://launchpad.test/%s/+series/devel" % ( |
205 | + distroseries.distribution.name), |
206 | + "http://launchpad.test/%s/+series/%s" % ( |
207 | + distroseries.distribution.name, distroseries.name)) |
208 | + |
209 | def test_new_series_url_redirects(self): |
210 | distroseries = self.factory.makeDistroSeries() |
211 | self.assertRedirects( |
212 | @@ -97,6 +133,54 @@ class TestDistributionNavigation(TestCaseWithFactory): |
213 | self.assertIsInstance(marshaller.dereference_url(url), RedirectionView) |
214 | self.assertEqual(expected_obj, marshaller.marshall_from_json_data(url)) |
215 | |
216 | + def test_classic_series_url_supports_object_lookup(self): |
217 | + # Classic series URLs (without +series) are compatible with |
218 | + # webservice object lookup, even if the distribution is configured |
219 | + # to redirect the default traversal. |
220 | + distroseries = self.factory.makeDistroSeries() |
221 | + distroseries.distribution.redirect_default_traversal = True |
222 | + distroseries_url = "/%s/%s" % ( |
223 | + distroseries.distribution.name, distroseries.name) |
224 | + self.assertDereferences(distroseries_url, distroseries) |
225 | + |
226 | + # Objects subordinate to the redirected series work too. |
227 | + distroarchseries = self.factory.makeDistroArchSeries( |
228 | + distroseries=distroseries) |
229 | + distroarchseries_url = "/%s/%s/%s" % ( |
230 | + distroarchseries.distroseries.distribution.name, |
231 | + distroarchseries.distroseries.name, |
232 | + distroarchseries.architecturetag) |
233 | + self.assertDereferences(distroarchseries_url, distroarchseries) |
234 | + |
235 | + def test_classic_series_url_supports_object_lookup_https(self): |
236 | + # Classic series URLs (without +series) are compatible with |
237 | + # webservice object lookup, even if the distribution is configured |
238 | + # to redirect the default traversal and the vhost is configured to |
239 | + # use HTTPS. "SERVER_URL": None exposes a bug in lazr.restful < |
240 | + # 0.22.2. |
241 | + self.addCleanup(allvhosts.reload) |
242 | + self.pushConfig("vhosts", use_https=True) |
243 | + allvhosts.reload() |
244 | + |
245 | + distroseries = self.factory.makeDistroSeries() |
246 | + distroseries.distribution.redirect_default_traversal = True |
247 | + distroseries_url = "/%s/%s" % ( |
248 | + distroseries.distribution.name, distroseries.name) |
249 | + self.assertDereferences( |
250 | + distroseries_url, distroseries, |
251 | + environ={"HTTPS": "on", "SERVER_URL": None}) |
252 | + |
253 | + # Objects subordinate to the redirected series work too. |
254 | + distroarchseries = self.factory.makeDistroArchSeries( |
255 | + distroseries=distroseries) |
256 | + distroarchseries_url = "/%s/%s/%s" % ( |
257 | + distroarchseries.distroseries.distribution.name, |
258 | + distroarchseries.distroseries.name, |
259 | + distroarchseries.architecturetag) |
260 | + self.assertDereferences( |
261 | + distroarchseries_url, distroarchseries, |
262 | + environ={"HTTPS": "on", "SERVER_URL": None}) |
263 | + |
264 | def test_new_series_url_supports_object_lookup(self): |
265 | # New-style +series URLs are compatible with webservice object |
266 | # lookup. |
267 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
268 | index 1c73f4b..659b8d9 100644 |
269 | --- a/lib/lp/registry/configure.zcml |
270 | +++ b/lib/lp/registry/configure.zcml |
271 | @@ -1820,6 +1820,7 @@ |
272 | set_attributes=" |
273 | bug_reported_acknowledgement |
274 | bug_reporting_guidelines |
275 | + default_traversal_policy |
276 | description |
277 | development_series_alias |
278 | display_name |
279 | @@ -1837,6 +1838,7 @@ |
280 | official_malone |
281 | owner |
282 | package_derivatives_email |
283 | + redirect_default_traversal |
284 | redirect_release_uploads |
285 | summary |
286 | vcs |
287 | diff --git a/lib/lp/registry/enums.py b/lib/lp/registry/enums.py |
288 | index 631ba6f..30f4a79 100644 |
289 | --- a/lib/lp/registry/enums.py |
290 | +++ b/lib/lp/registry/enums.py |
291 | @@ -1,4 +1,4 @@ |
292 | -# Copyright 2010-2015 Canonical Ltd. This software is licensed under the |
293 | +# Copyright 2010-2020 Canonical Ltd. This software is licensed under the |
294 | # GNU Affero General Public License version 3 (see the file LICENSE). |
295 | |
296 | """Enums for the Registry app.""" |
297 | @@ -7,6 +7,7 @@ __metaclass__ = type |
298 | __all__ = [ |
299 | 'BranchSharingPolicy', |
300 | 'BugSharingPolicy', |
301 | + 'DistributionDefaultTraversalPolicy', |
302 | 'DistroSeriesDifferenceStatus', |
303 | 'DistroSeriesDifferenceType', |
304 | 'EXCLUSIVE_TEAM_POLICY', |
305 | @@ -425,3 +426,19 @@ class VCSType(DBEnumeratedType): |
306 | |
307 | The Git DVCS is used as the default project or distribution VCS. |
308 | """) |
309 | + |
310 | + |
311 | +class DistributionDefaultTraversalPolicy(DBEnumeratedType): |
312 | + """Policy for the default traversal from a distribution. |
313 | + |
314 | + This determines what the "name" segment in a URL such as |
315 | + "/{distro}/{name}" (with no intervening segment such as "+source") |
316 | + means. |
317 | + """ |
318 | + |
319 | + SERIES = DBItem(0, """ |
320 | + Series |
321 | + |
322 | + The default traversal from a distribution is used for series of that |
323 | + distribution. |
324 | + """) |
325 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
326 | index ecd6c49..8c23b90 100644 |
327 | --- a/lib/lp/registry/interfaces/distribution.py |
328 | +++ b/lib/lp/registry/interfaces/distribution.py |
329 | @@ -73,7 +73,10 @@ from lp.bugs.interfaces.bugtarget import ( |
330 | from lp.bugs.interfaces.structuralsubscription import ( |
331 | IStructuralSubscriptionTarget, |
332 | ) |
333 | -from lp.registry.enums import VCSType |
334 | +from lp.registry.enums import ( |
335 | + DistributionDefaultTraversalPolicy, |
336 | + VCSType, |
337 | + ) |
338 | from lp.registry.interfaces.announcement import IMakesAnnouncements |
339 | from lp.registry.interfaces.distributionmirror import IDistributionMirror |
340 | from lp.registry.interfaces.distroseries import DistroSeriesNameField |
341 | @@ -390,6 +393,20 @@ class IDistributionPublic( |
342 | description=_( |
343 | "Version control system for this distribution's code."))) |
344 | |
345 | + default_traversal_policy = exported(Choice( |
346 | + title=_("Default traversal policy"), |
347 | + description=_( |
348 | + "The type of object that /{distro}/{name} URLs for this " |
349 | + "distribution resolve to."), |
350 | + vocabulary=DistributionDefaultTraversalPolicy, |
351 | + readonly=False, required=False)) |
352 | + redirect_default_traversal = exported(Bool( |
353 | + title=_("Redirect the default traversal"), |
354 | + description=_( |
355 | + "If true, the default traversal is for migration and redirects " |
356 | + "to a different canonical URL."), |
357 | + readonly=False, required=False)) |
358 | + |
359 | def getArchiveIDList(archive=None): |
360 | """Return a list of archive IDs suitable for sqlvalues() or quote(). |
361 | |
362 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
363 | index b9baba7..ba9b432 100644 |
364 | --- a/lib/lp/registry/model/distribution.py |
365 | +++ b/lib/lp/registry/model/distribution.py |
366 | @@ -88,6 +88,7 @@ from lp.code.interfaces.seriessourcepackagebranch import ( |
367 | from lp.registry.enums import ( |
368 | BranchSharingPolicy, |
369 | BugSharingPolicy, |
370 | + DistributionDefaultTraversalPolicy, |
371 | SpecificationSharingPolicy, |
372 | VCSType, |
373 | ) |
374 | @@ -262,6 +263,10 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements, |
375 | redirect_release_uploads = BoolCol(notNull=True, default=False) |
376 | development_series_alias = StringCol(notNull=False, default=None) |
377 | vcs = EnumCol(enum=VCSType, notNull=False) |
378 | + default_traversal_policy = EnumCol( |
379 | + enum=DistributionDefaultTraversalPolicy, notNull=False, |
380 | + default=DistributionDefaultTraversalPolicy.SERIES) |
381 | + redirect_default_traversal = BoolCol(notNull=False, default=False) |
382 | |
383 | def __repr__(self): |
384 | display_name = self.display_name.encode('ASCII', 'backslashreplace') |
385 | diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py |
386 | index 9eb671d..a053738 100644 |
387 | --- a/lib/lp/registry/tests/test_distribution.py |
388 | +++ b/lib/lp/registry/tests/test_distribution.py |
389 | @@ -31,6 +31,7 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
390 | from lp.registry.enums import ( |
391 | BranchSharingPolicy, |
392 | BugSharingPolicy, |
393 | + DistributionDefaultTraversalPolicy, |
394 | EXCLUSIVE_TEAM_POLICY, |
395 | INCLUSIVE_TEAM_POLICY, |
396 | ) |
397 | @@ -345,6 +346,31 @@ class TestDistribution(TestCaseWithFactory): |
398 | self.assertEqual(1, result.count()) |
399 | self.assertEqual(first_project, result[0]) |
400 | |
401 | + def test_default_traversal(self): |
402 | + # By default, a distribution's default traversal refers to its |
403 | + # series. |
404 | + distro = self.factory.makeDistribution() |
405 | + self.assertEqual( |
406 | + DistributionDefaultTraversalPolicy.SERIES, |
407 | + distro.default_traversal_policy) |
408 | + self.assertFalse(distro.redirect_default_traversal) |
409 | + |
410 | + def test_default_traversal_permissions(self): |
411 | + # Only distribution owners can change the default traversal |
412 | + # behaviour. |
413 | + distro = self.factory.makeDistribution() |
414 | + with person_logged_in(self.factory.makePerson()): |
415 | + self.assertRaises( |
416 | + Unauthorized, setattr, distro, 'default_traversal_policy', |
417 | + DistributionDefaultTraversalPolicy.SERIES) |
418 | + self.assertRaises( |
419 | + Unauthorized, setattr, distro, 'redirect_default_traversal', |
420 | + True) |
421 | + with person_logged_in(distro.owner): |
422 | + distro.default_traversal_policy = ( |
423 | + DistributionDefaultTraversalPolicy.SERIES) |
424 | + distro.redirect_default_traversal = True |
425 | + |
426 | |
427 | class TestDistributionCurrentSourceReleases( |
428 | CurrentSourceReleasesMixin, TestCase): |
LGTM