Merge ~pappacena/launchpad:snap-pillar-product-url into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: bc1a2e1b0a29aee11af404f4d02e3346eae45244
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-pillar-product-url
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:snap-pillar-list-filters
Diff against target: 265 lines (+94/-14)
9 files modified
lib/lp/registry/browser/person.py (+6/-2)
lib/lp/registry/browser/personproduct.py (+9/-1)
lib/lp/snappy/browser/configure.zcml (+1/-2)
lib/lp/snappy/browser/snap.py (+27/-1)
lib/lp/snappy/browser/tests/test_snap.py (+10/-3)
lib/lp/snappy/interfaces/snap.py (+3/-0)
lib/lp/snappy/model/snap.py (+12/-0)
lib/lp/snappy/tests/test_snap.py (+24/-4)
lib/lp/snappy/tests/test_snapbuild.py (+2/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+399025@code.launchpad.net

Commit message

Redirecting snaps with pillar to stay under ~user/pillar URL

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Information
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes. This should be ready for another round of review.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
2index b15ec69..002b94a 100644
3--- a/lib/lp/registry/browser/person.py
4+++ b/lib/lp/registry/browser/person.py
5@@ -1,4 +1,4 @@
6-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
7+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """Person-related view classes."""
11@@ -646,7 +646,11 @@ class PersonNavigation(BranchTraversalMixin, Navigation):
12 @stepthrough('+snap')
13 def traverse_snap(self, name):
14 """Traverse to this person's snap packages."""
15- return getUtility(ISnapSet).getByName(self.context, name)
16+ snap = getUtility(ISnapSet).getByPillarAndName(
17+ self.context, None, name)
18+ if snap is None:
19+ raise NotFoundError(name)
20+ return snap
21
22
23 class PersonSetNavigation(Navigation):
24diff --git a/lib/lp/registry/browser/personproduct.py b/lib/lp/registry/browser/personproduct.py
25index a249b27..7d850e3 100644
26--- a/lib/lp/registry/browser/personproduct.py
27+++ b/lib/lp/registry/browser/personproduct.py
28@@ -1,4 +1,4 @@
29-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
30+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
31 # GNU Affero General Public License version 3 (see the file LICENSE).
32
33 """Views, menus and traversal related to PersonProducts."""
34@@ -30,6 +30,7 @@ from lp.services.webapp import (
35 )
36 from lp.services.webapp.breadcrumb import Breadcrumb
37 from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb
38+from lp.snappy.interfaces.snap import ISnapSet
39
40
41 class PersonProductNavigation(PersonTargetDefaultVCSNavigationMixin,
42@@ -53,6 +54,13 @@ class PersonProductNavigation(PersonTargetDefaultVCSNavigationMixin,
43 else:
44 return branch
45
46+ @stepthrough('+snap')
47+ def traverse_snap(self, name):
48+ return getUtility(ISnapSet).getByPillarAndName(
49+ owner=self.context.person,
50+ pillar=self.context.product,
51+ name=name)
52+
53
54 @implementer(IMultiFacetedBreadcrumb)
55 class PersonProductBreadcrumb(Breadcrumb):
56diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml
57index b11f797..ebbbb66 100644
58--- a/lib/lp/snappy/browser/configure.zcml
59+++ b/lib/lp/snappy/browser/configure.zcml
60@@ -11,8 +11,7 @@
61 <facet facet="overview">
62 <browser:url
63 for="lp.snappy.interfaces.snap.ISnap"
64- path_expression="string:+snap/${name}"
65- attribute_to_parent="owner" />
66+ urldata="lp.snappy.browser.snap.SnapURL" />
67 <browser:defaultView
68 for="lp.snappy.interfaces.snap.ISnap"
69 name="+index" />
70diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
71index ab4ebf0..c51356e 100644
72--- a/lib/lp/snappy/browser/snap.py
73+++ b/lib/lp/snappy/browser/snap.py
74@@ -28,7 +28,10 @@ from six.moves.urllib.parse import urlencode
75 from zope.component import getUtility
76 from zope.error.interfaces import IErrorReportingUtility
77 from zope.formlib.widget import CustomWidgetFactory
78-from zope.interface import Interface
79+from zope.interface import (
80+ implementer,
81+ Interface,
82+ )
83 from zope.schema import (
84 Choice,
85 Dict,
86@@ -61,6 +64,7 @@ from lp.code.browser.widgets.gitref import GitRefWidget
87 from lp.code.interfaces.gitref import IGitRef
88 from lp.registry.enums import VCSType
89 from lp.registry.interfaces.person import IPersonSet
90+from lp.registry.interfaces.personproduct import IPersonProductFactory
91 from lp.registry.interfaces.pocket import PackagePublishingPocket
92 from lp.services.features import getFeatureFlag
93 from lp.services.propertycache import cachedproperty
94@@ -81,6 +85,7 @@ from lp.services.webapp.breadcrumb import (
95 Breadcrumb,
96 NameBreadcrumb,
97 )
98+from lp.services.webapp.interfaces import ICanonicalUrlData
99 from lp.services.webapp.url import urlappend
100 from lp.services.webhooks.browser import WebhookTargetNavigationMixin
101 from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget
102@@ -112,6 +117,27 @@ from lp.soyuz.browser.build import get_build_by_id_str
103 from lp.soyuz.interfaces.archive import IArchive
104
105
106+@implementer(ICanonicalUrlData)
107+class SnapURL:
108+ """Snap URL creation rules."""
109+ rootsite = 'mainsite'
110+
111+ def __init__(self, snap):
112+ self.snap = snap
113+
114+ @property
115+ def inside(self):
116+ owner = self.snap.owner
117+ project = self.snap.project
118+ if project is None:
119+ return owner
120+ return getUtility(IPersonProductFactory).create(owner, project)
121+
122+ @property
123+ def path(self):
124+ return "+snap/%s" % self.snap.name
125+
126+
127 class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
128 usedfor = ISnap
129
130diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
131index c57f412..f6a1cfc 100644
132--- a/lib/lp/snappy/browser/tests/test_snap.py
133+++ b/lib/lp/snappy/browser/tests/test_snap.py
134@@ -572,9 +572,7 @@ class TestSnapAddView(BaseTestSnapView):
135 self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
136 parsed_location = urlsplit(browser.headers["Location"])
137 self.assertEqual(
138- urlsplit(
139- canonical_url(snap, rootsite="code") +
140- "/+authorize/+login")[:3],
141+ urlsplit(canonical_url(snap) + "/+authorize/+login")[:3],
142 parsed_location[:3])
143 expected_args = {
144 "discharge_macaroon_action": ["field.actions.complete"],
145@@ -1521,6 +1519,15 @@ class TestSnapView(BaseTestSnapView):
146 "snap breadcrumb", "li",
147 text=re.compile(r"\ssnap-name\s")))))
148
149+ def test_snap_with_project_pillar_url(self):
150+ project = self.factory.makeProduct()
151+ snap = self.factory.makeSnap(project=project)
152+ browser = self.getViewBrowser(snap)
153+ with admin_logged_in():
154+ expected_url = 'http://launchpad.test/~{}/{}/+snap/{}'.format(
155+ snap.owner.name, project.name, snap.name)
156+ self.assertEqual(expected_url, browser.url)
157+
158 def test_index_bzr(self):
159 branch = self.factory.makePersonalBranch(
160 owner=self.person, name="snap-branch")
161diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
162index 4bef0ef..28f0824 100644
163--- a/lib/lp/snappy/interfaces/snap.py
164+++ b/lib/lp/snappy/interfaces/snap.py
165@@ -965,6 +965,9 @@ class ISnapSet(Interface):
166 def getByName(owner, name):
167 """Return the appropriate `ISnap` for the given objects."""
168
169+ def getByPillarAndName(owner, pillar, name):
170+ """Returns the appropriate `ISnap` for the given pillar and name."""
171+
172 @operation_parameters(
173 owner=Reference(IPerson, title=_("Owner"), required=True))
174 @operation_returns_collection_of(ISnap)
175diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
176index 27f6694..5b1be23 100644
177--- a/lib/lp/snappy/model/snap.py
178+++ b/lib/lp/snappy/model/snap.py
179@@ -1439,6 +1439,18 @@ class SnapSet:
180 raise NoSuchSnap(name)
181 return snap
182
183+ def getByPillarAndName(self, owner, pillar, name):
184+ conditions = [Snap.owner == owner, Snap.name == name]
185+ if pillar is None:
186+ # If we start supporting more pillars, remember to add the
187+ # conditions here.
188+ conditions.append(Snap.project == None)
189+ elif IProduct.providedBy(pillar):
190+ conditions.append(Snap.project == pillar)
191+ else:
192+ raise NotImplementedError("Unknown pillar for snap: %s" % pillar)
193+ return IStore(Snap).find(Snap, *conditions).one()
194+
195 def _getSnapsFromCollection(self, collection, owner=None,
196 visible_by_user=None):
197 if IBranchCollection.providedBy(collection):
198diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
199index 0c6f68e..5e31742 100644
200--- a/lib/lp/snappy/tests/test_snap.py
201+++ b/lib/lp/snappy/tests/test_snap.py
202@@ -1684,6 +1684,22 @@ class TestSnapSet(TestCaseWithFactory):
203 getUtility(ISnapSet).exists(self.factory.makePerson(), snap.name))
204 self.assertFalse(getUtility(ISnapSet).exists(snap.owner, "different"))
205
206+ def test_getByPillarAndName(self):
207+ owner = self.factory.makePerson()
208+ project = self.factory.makeProduct()
209+ project_snap = self.factory.makeSnap(
210+ name='proj-snap', owner=owner, registrant=owner, project=project)
211+ no_project_snap = self.factory.makeSnap(
212+ name='no-proj-snap', owner=owner, registrant=owner)
213+
214+ snap_set = getUtility(ISnapSet)
215+ self.assertEqual(
216+ project_snap,
217+ snap_set.getByPillarAndName(owner, project, 'proj-snap'))
218+ self.assertEqual(
219+ no_project_snap,
220+ snap_set.getByPillarAndName(owner, None, 'no-proj-snap'))
221+
222 def test_findByOwner(self):
223 # ISnapSet.findByOwner returns all Snaps with the given owner.
224 owners = [self.factory.makePerson() for i in range(2)]
225@@ -2877,9 +2893,13 @@ class TestSnapWebservice(TestCaseWithFactory):
226 branch = self.factory.makeAnyBranch(
227 owner=self.person,
228 information_type=InformationType.PRIVATESECURITY)
229+ project = self.factory.makeProduct(
230+ owner=self.person, registrant=self.person,
231+ information_type=InformationType.PROPRIETARY,
232+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
233 snap = self.factory.makeSnap(
234 registrant=self.person, owner=self.person, branch=branch,
235- private=True)
236+ project=project, information_type=InformationType.PROPRIETARY)
237 admin = getUtility(ILaunchpadCelebrities).admin.teamowner
238 with person_logged_in(self.person):
239 snap_url = api_url(snap)
240@@ -2887,9 +2907,9 @@ class TestSnapWebservice(TestCaseWithFactory):
241 admin_webservice = webservice_for_person(
242 admin, permission=OAuthPermission.WRITE_PRIVATE)
243 admin_webservice.default_api_version = "devel"
244- response = admin_webservice.patch(
245- snap_url, "application/json",
246- json.dumps({"information_type": 'Public'}))
247+ data = json.dumps({"information_type": 'Public'})
248+ content_type = "application/json"
249+ response = admin_webservice.patch(snap_url, content_type, data)
250 self.assertEqual(400, response.status)
251 self.assertEqual(
252 b"Snap recipe contains private information and cannot be public.",
253diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py
254index 7acff54..ee67203 100644
255--- a/lib/lp/snappy/tests/test_snapbuild.py
256+++ b/lib/lp/snappy/tests/test_snapbuild.py
257@@ -789,7 +789,8 @@ class TestSnapBuildWebservice(TestCaseWithFactory):
258 self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
259 unpriv_webservice.default_api_version = "devel"
260 logout()
261- self.assertEqual(200, self.webservice.get(build_url).status)
262+ response = self.webservice.get(build_url)
263+ self.assertEqual(200, response.status)
264 # 404 since we aren't allowed to know that the private team exists.
265 self.assertEqual(404, unpriv_webservice.get(build_url).status)
266