Merge lp:~cjwatson/launchpad/snap-extend-find-by-url into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18324
Proposed branch: lp:~cjwatson/launchpad/snap-extend-find-by-url
Merge into: lp:launchpad
Diff against target: 295 lines (+190/-22)
3 files modified
lib/lp/snappy/interfaces/snap.py (+25/-2)
lib/lp/snappy/model/snap.py (+20/-9)
lib/lp/snappy/tests/test_snap.py (+145/-11)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-extend-find-by-url
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+316338@code.launchpad.net

Commit message

Allow limiting SnapSet.findByURL results by owner; add SnapSet.findByURLPrefix.

Description of the change

build.snapcraft.io is going to need to show a dashboard of a user's repositories, including which of them are already enabled. We'll of course need to do lots of caching, but we'll also need a bulk interface to let us discover which snaps exist in Launchpad for a set of GitHub repositories. I think it's best to just do this by URL prefix.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/snappy/interfaces/snap.py'
2--- lib/lp/snappy/interfaces/snap.py 2017-02-01 06:31:30 +0000
3+++ lib/lp/snappy/interfaces/snap.py 2017-02-03 14:33:36 +0000
4@@ -655,12 +655,14 @@
5 :raises BadSnapSearchContext: if the context is not understood.
6 """
7
8- @operation_parameters(url=TextLine(title=_("The URL to search for.")))
9+ @operation_parameters(
10+ url=TextLine(title=_("The URL to search for.")),
11+ owner=Reference(IPerson, title=_("Owner"), required=False))
12 @call_with(visible_by_user=REQUEST_USER)
13 @operation_returns_collection_of(ISnap)
14 @export_read_operation()
15 @operation_for_version("devel")
16- def findByURL(url, visible_by_user=None):
17+ def findByURL(url, owner=None, visible_by_user=None):
18 """Return all snap packages that build from the given URL.
19
20 This currently only works for packages that build directly from a
21@@ -668,6 +670,27 @@
22 hosted in Launchpad.
23
24 :param url: A URL.
25+ :param owner: Only return packages owned by this user.
26+ :param visible_by_user: If not None, only return packages visible by
27+ this user; otherwise, only return publicly-visible packages.
28+ """
29+
30+ @operation_parameters(
31+ url_prefix=TextLine(title=_("The URL prefix to search for.")),
32+ owner=Reference(IPerson, title=_("Owner"), required=False))
33+ @call_with(visible_by_user=REQUEST_USER)
34+ @operation_returns_collection_of(ISnap)
35+ @export_read_operation()
36+ @operation_for_version("devel")
37+ def findByURLPrefix(url_prefix, owner=None, visible_by_user=None):
38+ """Return all snap packages that build from a URL with this prefix.
39+
40+ This currently only works for packages that build directly from a
41+ URL, rather than being linked to a Bazaar branch or Git repository
42+ hosted in Launchpad.
43+
44+ :param url_prefix: A URL prefix.
45+ :param owner: Only return packages owned by this user.
46 :param visible_by_user: If not None, only return packages visible by
47 this user; otherwise, only return publicly-visible packages.
48 """
49
50=== modified file 'lib/lp/snappy/model/snap.py'
51--- lib/lp/snappy/model/snap.py 2017-01-04 22:59:04 +0000
52+++ lib/lp/snappy/model/snap.py 2017-02-03 14:33:36 +0000
53@@ -799,26 +799,37 @@
54 snaps.order_by(Desc(Snap.date_last_modified))
55 return snaps
56
57- def findByURL(self, url, visible_by_user=None):
58- """See `ISnapSet`."""
59- clauses = [Snap.git_repository_url == url]
60+ def _findByURLVisibilityClause(self, visible_by_user):
61 # XXX cjwatson 2016-11-25: This is in principle a poor query, but we
62 # don't yet have the access grant infrastructure to do better, and
63- # in any case since we're querying for a single URL the numbers
64- # involved should be very small.
65+ # in any case the numbers involved should be very small.
66 if visible_by_user is None:
67- visibility_clause = Snap.private == False
68+ return Snap.private == False
69 else:
70 roles = IPersonRoles(visible_by_user)
71 if roles.in_admin or roles.in_commercial_admin:
72- visibility_clause = True
73+ return True
74 else:
75- visibility_clause = Or(
76+ return Or(
77 Snap.private == False,
78 Snap.owner_id.is_in(Select(
79 TeamParticipation.teamID,
80 TeamParticipation.person == visible_by_user)))
81- clauses.append(visibility_clause)
82+
83+ def findByURL(self, url, owner=None, visible_by_user=None):
84+ """See `ISnapSet`."""
85+ clauses = [Snap.git_repository_url == url]
86+ if owner is not None:
87+ clauses.append(Snap.owner == owner)
88+ clauses.append(self._findByURLVisibilityClause(visible_by_user))
89+ return IStore(Snap).find(Snap, *clauses)
90+
91+ def findByURLPrefix(self, url_prefix, owner=None, visible_by_user=None):
92+ """See `ISnapSet`."""
93+ clauses = [Snap.git_repository_url.startswith(url_prefix)]
94+ if owner is not None:
95+ clauses.append(Snap.owner == owner)
96+ clauses.append(self._findByURLVisibilityClause(visible_by_user))
97 return IStore(Snap).find(Snap, *clauses)
98
99 def preloadDataForSnaps(self, snaps, user=None):
100
101=== modified file 'lib/lp/snappy/tests/test_snap.py'
102--- lib/lp/snappy/tests/test_snap.py 2017-01-04 20:52:12 +0000
103+++ lib/lp/snappy/tests/test_snap.py 2017-02-03 14:33:36 +0000
104@@ -832,16 +832,48 @@
105 def test_findByURL(self):
106 # ISnapSet.findByURL returns visible Snaps with the given URL.
107 urls = [u"https://git.example.org/foo", u"https://git.example.org/bar"]
108- snaps = []
109- for url in urls:
110- snaps.append(self.factory.makeSnap(
111- git_ref=self.factory.makeGitRefRemote(repository_url=url)))
112- snaps.append(
113- self.factory.makeSnap(branch=self.factory.makeAnyBranch()))
114- snaps.append(
115- self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]))
116- self.assertContentEqual(
117- [snaps[0]], getUtility(ISnapSet).findByURL(urls[0]))
118+ owners = [self.factory.makePerson() for i in range(2)]
119+ snaps = []
120+ for url in urls:
121+ for owner in owners:
122+ snaps.append(self.factory.makeSnap(
123+ registrant=owner, owner=owner,
124+ git_ref=self.factory.makeGitRefRemote(repository_url=url)))
125+ snaps.append(
126+ self.factory.makeSnap(branch=self.factory.makeAnyBranch()))
127+ snaps.append(
128+ self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]))
129+ self.assertContentEqual(
130+ snaps[:2], getUtility(ISnapSet).findByURL(urls[0]))
131+ self.assertContentEqual(
132+ [snaps[0]],
133+ getUtility(ISnapSet).findByURL(urls[0], owner=owners[0]))
134+
135+ def test_findByURLPrefix(self):
136+ # ISnapSet.findByURLPrefix returns visible Snaps with the given URL
137+ # prefix.
138+ urls = [
139+ u"https://git.example.org/foo/a",
140+ u"https://git.example.org/foo/b",
141+ u"https://git.example.org/bar",
142+ ]
143+ owners = [self.factory.makePerson() for i in range(2)]
144+ snaps = []
145+ for url in urls:
146+ for owner in owners:
147+ snaps.append(self.factory.makeSnap(
148+ registrant=owner, owner=owner,
149+ git_ref=self.factory.makeGitRefRemote(repository_url=url)))
150+ snaps.append(
151+ self.factory.makeSnap(branch=self.factory.makeAnyBranch()))
152+ snaps.append(
153+ self.factory.makeSnap(git_ref=self.factory.makeGitRefs()[0]))
154+ prefix = u"https://git.example.org/foo/"
155+ self.assertContentEqual(
156+ snaps[:4], getUtility(ISnapSet).findByURLPrefix(prefix))
157+ self.assertContentEqual(
158+ [snaps[0], snaps[2]],
159+ getUtility(ISnapSet).findByURLPrefix(prefix, owner=owners[0]))
160
161 def test__findStaleSnaps(self):
162 # Stale; not built automatically.
163@@ -1325,8 +1357,10 @@
164 for private in (False, True):
165 ref = self.factory.makeGitRefRemote(repository_url=url)
166 snaps.append(self.factory.makeSnap(
167- registrant=person, git_ref=ref, private=private))
168+ registrant=person, owner=person, git_ref=ref,
169+ private=private))
170 with admin_logged_in():
171+ person_urls = [api_url(person) for person in persons]
172 ws_snaps = [
173 self.webservice.getAbsoluteUrl(api_url(snap))
174 for snap in snaps]
175@@ -1341,6 +1375,13 @@
176 self.assertContentEqual(
177 [ws_snaps[0], ws_snaps[2]],
178 [entry["self_link"] for entry in response.jsonBody()["entries"]])
179+ response = anon_webservice.named_get(
180+ "/+snaps", "findByURL", url=urls[0], owner=person_urls[0],
181+ api_version="devel")
182+ self.assertEqual(200, response.status)
183+ self.assertContentEqual(
184+ [ws_snaps[0]],
185+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
186 # persons[0] can see both public snaps with this URL, as well as
187 # their own private snap.
188 webservice = webservice_for_person(
189@@ -1351,6 +1392,13 @@
190 self.assertContentEqual(
191 ws_snaps[:3],
192 [entry["self_link"] for entry in response.jsonBody()["entries"]])
193+ response = webservice.named_get(
194+ "/+snaps", "findByURL", url=urls[0], owner=person_urls[0],
195+ api_version="devel")
196+ self.assertEqual(200, response.status)
197+ self.assertContentEqual(
198+ ws_snaps[:2],
199+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
200 # Admins can see all snaps with this URL.
201 commercial_admin_webservice = webservice_for_person(
202 commercial_admin, permission=OAuthPermission.READ_PRIVATE)
203@@ -1360,6 +1408,92 @@
204 self.assertContentEqual(
205 ws_snaps[:4],
206 [entry["self_link"] for entry in response.jsonBody()["entries"]])
207+ response = commercial_admin_webservice.named_get(
208+ "/+snaps", "findByURL", url=urls[0], owner=person_urls[0],
209+ api_version="devel")
210+ self.assertEqual(200, response.status)
211+ self.assertContentEqual(
212+ ws_snaps[:2],
213+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
214+
215+ def test_findByURLPrefix(self):
216+ # lp.snaps.findByURLPrefix returns visible Snaps with the given URL
217+ # prefix.
218+ self.pushConfig("launchpad", default_batch_size=10)
219+ persons = [self.factory.makePerson(), self.factory.makePerson()]
220+ urls = [
221+ u"https://git.example.org/foo/a",
222+ u"https://git.example.org/foo/b",
223+ u"https://git.example.org/bar",
224+ ]
225+ snaps = []
226+ for url in urls:
227+ for person in persons:
228+ for private in (False, True):
229+ ref = self.factory.makeGitRefRemote(repository_url=url)
230+ snaps.append(self.factory.makeSnap(
231+ registrant=person, owner=person, git_ref=ref,
232+ private=private))
233+ with admin_logged_in():
234+ person_urls = [api_url(person) for person in persons]
235+ ws_snaps = [
236+ self.webservice.getAbsoluteUrl(api_url(snap))
237+ for snap in snaps]
238+ commercial_admin = (
239+ getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
240+ logout()
241+ prefix = u"https://git.example.org/foo/"
242+ # Anonymous requests can only see public snaps.
243+ anon_webservice = LaunchpadWebServiceCaller("test", "")
244+ response = anon_webservice.named_get(
245+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
246+ api_version="devel")
247+ self.assertEqual(200, response.status)
248+ self.assertContentEqual(
249+ [ws_snaps[i] for i in (0, 2, 4, 6)],
250+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
251+ response = anon_webservice.named_get(
252+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
253+ owner=person_urls[0], api_version="devel")
254+ self.assertEqual(200, response.status)
255+ self.assertContentEqual(
256+ [ws_snaps[i] for i in (0, 4)],
257+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
258+ # persons[0] can see all public snaps with this URL prefix, as well
259+ # as their own matching private snaps.
260+ webservice = webservice_for_person(
261+ persons[0], permission=OAuthPermission.READ_PRIVATE)
262+ response = webservice.named_get(
263+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
264+ api_version="devel")
265+ self.assertEqual(200, response.status)
266+ self.assertContentEqual(
267+ [ws_snaps[i] for i in (0, 1, 2, 4, 5, 6)],
268+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
269+ response = webservice.named_get(
270+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
271+ owner=person_urls[0], api_version="devel")
272+ self.assertEqual(200, response.status)
273+ self.assertContentEqual(
274+ [ws_snaps[i] for i in (0, 1, 4, 5)],
275+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
276+ # Admins can see all snaps with this URL prefix.
277+ commercial_admin_webservice = webservice_for_person(
278+ commercial_admin, permission=OAuthPermission.READ_PRIVATE)
279+ response = commercial_admin_webservice.named_get(
280+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
281+ api_version="devel")
282+ self.assertEqual(200, response.status)
283+ self.assertContentEqual(
284+ ws_snaps[:8],
285+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
286+ response = commercial_admin_webservice.named_get(
287+ "/+snaps", "findByURLPrefix", url_prefix=prefix,
288+ owner=person_urls[0], api_version="devel")
289+ self.assertEqual(200, response.status)
290+ self.assertContentEqual(
291+ [ws_snaps[i] for i in (0, 1, 4, 5)],
292+ [entry["self_link"] for entry in response.jsonBody()["entries"]])
293
294 def setProcessors(self, user, snap, names):
295 ws = webservice_for_person(