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