Merge lp:~cjwatson/launchpad/person-dsp into lp:launchpad

Proposed by Colin Watson on 2015-02-06
Status: Merged
Approved by: Colin Watson on 2015-02-12
Approved revision: no longer in the source branch.
Merged at revision: 17340
Proposed branch: lp:~cjwatson/launchpad/person-dsp
Merge into: lp:launchpad
Diff against target: 336 lines (+247/-4)
7 files modified
lib/lp/registry/browser/configure.zcml (+14/-1)
lib/lp/registry/browser/person.py (+27/-2)
lib/lp/registry/browser/persondistributionsourcepackage.py (+69/-0)
lib/lp/registry/configure.zcml (+22/-1)
lib/lp/registry/interfaces/persondistributionsourcepackage.py (+34/-0)
lib/lp/registry/model/persondistributionsourcepackage.py (+39/-0)
lib/lp/registry/tests/test_persondistributionsourcepackage.py (+42/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/person-dsp
Reviewer Review Type Date Requested Status
William Grant code 2015-02-06 Approve on 2015-02-09
Review via email: mp+248911@code.launchpad.net

Commit message

Introduce PersonDistributionSourcePackage, which will later serve as a target for Git repositories.

Description of the change

Introduce PersonDistributionSourcePackage, which will later serve as a target for Git repositories.

To post a comment you must log in.
William Grant (wgrant) wrote :

This looks fine apart from some potential breadcrumb improvements.

review: Approve (code)
Colin Watson (cjwatson) wrote :

I tend to agree that we'll likely need a PersonDistribution, but I'm finding it a little hard to tell right now because my Git repository browser code isn't far enough along. I'm going to go ahead and land this for now so that I can make progress on the model, and introduce PersonDistribution a little later if it indeed becomes necessary for the browser.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/configure.zcml'
2--- lib/lp/registry/browser/configure.zcml 2015-01-29 10:35:21 +0000
3+++ lib/lp/registry/browser/configure.zcml 2015-02-09 17:43:47 +0000
4@@ -1,4 +1,4 @@
5-<!-- Copyright 2009-2014 Canonical Ltd. This software is licensed under the
6+<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the
7 GNU Affero General Public License version 3 (see the file LICENSE).
8 -->
9
10@@ -2154,6 +2154,19 @@
11 />
12
13 <browser:url
14+ for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
15+ path_expression="string:${distro_source_package/distribution/name}/+source/${distro_source_package/sourcepackagename/name}"
16+ attribute_to_parent="person"/>
17+ <browser:navigation
18+ module="lp.registry.browser.persondistributionsourcepackage"
19+ classes="
20+ PersonDistributionSourcePackageNavigation"/>
21+ <browser:menus
22+ classes="
23+ PersonDistributionSourcePackageFacets"
24+ module="lp.registry.browser.persondistributionsourcepackage"/>
25+
26+ <browser:url
27 for="lp.registry.interfaces.personproduct.IPersonProduct"
28 path_expression="product/name"
29 attribute_to_parent="person"/>
30
31=== modified file 'lib/lp/registry/browser/person.py'
32--- lib/lp/registry/browser/person.py 2015-01-06 10:47:37 +0000
33+++ lib/lp/registry/browser/person.py 2015-02-09 17:43:47 +0000
34@@ -1,4 +1,4 @@
35-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
36+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
37 # GNU Affero General Public License version 3 (see the file LICENSE).
38
39 """Person-related view classes."""
40@@ -154,6 +154,7 @@
41 from lp.registry.enums import PersonVisibility
42 from lp.registry.errors import VoucherAlreadyRedeemed
43 from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
44+from lp.registry.interfaces.distribution import IDistribution
45 from lp.registry.interfaces.gpg import IGPGKeySet
46 from lp.registry.interfaces.irc import IIrcIDSet
47 from lp.registry.interfaces.jabber import (
48@@ -172,6 +173,9 @@
49 IPersonClaim,
50 IPersonSet,
51 )
52+from lp.registry.interfaces.persondistributionsourcepackage import (
53+ IPersonDistributionSourcePackageFactory,
54+ )
55 from lp.registry.interfaces.personproduct import IPersonProductFactory
56 from lp.registry.interfaces.persontransferjob import (
57 IPersonDeactivateJobSource,
58@@ -357,7 +361,9 @@
59 raise NotFoundError
60
61 def traverse(self, pillar_name):
62- # If the pillar is a product, then return the PersonProduct.
63+ # If the pillar is a product, then return the PersonProduct; if it
64+ # is a distribution and further segments provide a source package,
65+ # then return the PersonDistributionSourcePackage.
66 pillar = getUtility(IPillarNameSet).getByName(pillar_name)
67 if IProduct.providedBy(pillar):
68 person_product = getUtility(IPersonProductFactory).create(
69@@ -369,6 +375,25 @@
70 status=301)
71 getUtility(IOpenLaunchBag).add(pillar)
72 return person_product
73+ elif IDistribution.providedBy(pillar):
74+ if (len(self.request.stepstogo) >= 2 and
75+ self.request.stepstogo.peek() == "+source"):
76+ self.request.stepstogo.consume()
77+ spn_name = self.request.stepstogo.consume()
78+ dsp = IDistribution(pillar).getSourcePackage(spn_name)
79+ if dsp is not None:
80+ factory = getUtility(
81+ IPersonDistributionSourcePackageFactory)
82+ person_dsp = factory.create(self.context, dsp)
83+ # If accessed through an alias, redirect to the proper
84+ # name.
85+ if pillar.name != pillar_name:
86+ return self.redirectSubTree(
87+ canonical_url(person_dsp, request=self.request),
88+ status=301)
89+ getUtility(IOpenLaunchBag).add(pillar)
90+ return person_dsp
91+
92 # Otherwise look for a branch.
93 try:
94 branch = getUtility(IBranchNamespaceSet).traverse(
95
96=== added file 'lib/lp/registry/browser/persondistributionsourcepackage.py'
97--- lib/lp/registry/browser/persondistributionsourcepackage.py 1970-01-01 00:00:00 +0000
98+++ lib/lp/registry/browser/persondistributionsourcepackage.py 2015-02-09 17:43:47 +0000
99@@ -0,0 +1,69 @@
100+# Copyright 2015 Canonical Ltd. This software is licensed under the
101+# GNU Affero General Public License version 3 (see the file LICENSE).
102+
103+"""Views, menus and traversal related to PersonDistributionSourcePackages."""
104+
105+__metaclass__ = type
106+__all__ = [
107+ 'PersonDistributionSourcePackageBreadcrumb',
108+ 'PersonDistributionSourcePackageFacets',
109+ 'PersonDistributionSourcePackageNavigation',
110+ ]
111+
112+
113+from zope.component import queryAdapter
114+from zope.interface import implements
115+from zope.traversing.interfaces import IPathAdapter
116+
117+from lp.app.errors import NotFoundError
118+from lp.registry.interfaces.persondistributionsourcepackage import (
119+ IPersonDistributionSourcePackage,
120+ )
121+from lp.services.webapp import (
122+ canonical_url,
123+ Navigation,
124+ StandardLaunchpadFacets,
125+ )
126+from lp.services.webapp.breadcrumb import Breadcrumb
127+from lp.services.webapp.interfaces import IMultiFacetedBreadcrumb
128+
129+
130+class PersonDistributionSourcePackageNavigation(Navigation):
131+ usedfor = IPersonDistributionSourcePackage
132+
133+ def traverse(self, branch_name):
134+ # XXX cjwatson 2015-02-06: This will look for Git repositories in
135+ # the person/DSP namespace, but for now it does nothing.
136+ raise NotFoundError
137+
138+
139+# XXX cjwatson 2015-01-29: Do we need two breadcrumbs, one for the
140+# distribution and one for the source package?
141+class PersonDistributionSourcePackageBreadcrumb(Breadcrumb):
142+ """Breadcrumb for an `IPersonDistributionSourcePackage`."""
143+ implements(IMultiFacetedBreadcrumb)
144+
145+ @property
146+ def text(self):
147+ return self.context.distro_source_package.displayname
148+
149+ @property
150+ def url(self):
151+ if self._url is None:
152+ return canonical_url(
153+ self.context.distro_source_package, rootsite=self.rootsite)
154+ else:
155+ return self._url
156+
157+ @property
158+ def icon(self):
159+ return queryAdapter(
160+ self.context.distro_source_package, IPathAdapter,
161+ name='image').icon()
162+
163+
164+class PersonDistributionSourcePackageFacets(StandardLaunchpadFacets):
165+ """The links that will appear in the facet menu for an IPersonDSP."""
166+
167+ usedfor = IPersonDistributionSourcePackage
168+ enable_only = ['branches']
169
170=== modified file 'lib/lp/registry/configure.zcml'
171--- lib/lp/registry/configure.zcml 2015-01-29 16:28:30 +0000
172+++ lib/lp/registry/configure.zcml 2015-02-09 17:43:47 +0000
173@@ -1,4 +1,4 @@
174-<!-- Copyright 2009-2014 Canonical Ltd. This software is licensed under the
175+<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the
176 GNU Affero General Public License version 3 (see the file LICENSE).
177 -->
178
179@@ -2065,6 +2065,27 @@
180 interface="lp.registry.interfaces.packaging.IPackagingUtil"/>
181 </securedutility>
182
183+ <!-- PersonDistributionSourcePackage -->
184+
185+ <class
186+ class="lp.registry.model.persondistributionsourcepackage.PersonDistributionSourcePackage">
187+ <allow
188+ interface="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage" />
189+ </class>
190+
191+ <adapter
192+ provides="lp.services.webapp.interfaces.IBreadcrumb"
193+ for="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackage"
194+ factory="lp.registry.browser.persondistributionsourcepackage.PersonDistributionSourcePackageBreadcrumb"
195+ permission="zope.Public"/>
196+
197+ <securedutility
198+ component="lp.registry.model.persondistributionsourcepackage.PersonDistributionSourcePackage"
199+ provides="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackageFactory">
200+ <allow
201+ interface="lp.registry.interfaces.persondistributionsourcepackage.IPersonDistributionSourcePackageFactory"/>
202+ </securedutility>
203+
204 <!-- PersonNotification -->
205
206 <class
207
208=== added file 'lib/lp/registry/interfaces/persondistributionsourcepackage.py'
209--- lib/lp/registry/interfaces/persondistributionsourcepackage.py 1970-01-01 00:00:00 +0000
210+++ lib/lp/registry/interfaces/persondistributionsourcepackage.py 2015-02-09 17:43:47 +0000
211@@ -0,0 +1,34 @@
212+# Copyright 2015 Canonical Ltd. This software is licensed under the
213+# GNU Affero General Public License version 3 (see the file LICENSE).
214+
215+"""A person's view on a source package in a distribution."""
216+
217+__metaclass__ = type
218+__all__ = [
219+ 'IPersonDistributionSourcePackage',
220+ 'IPersonDistributionSourcePackageFactory',
221+ ]
222+
223+from lazr.restful.fields import Reference
224+from zope.interface import Interface
225+from zope.schema import TextLine
226+
227+from lp.registry.interfaces.person import IPerson
228+from lp.registry.interfaces.distributionsourcepackage import (
229+ IDistributionSourcePackage,
230+ )
231+
232+
233+class IPersonDistributionSourcePackage(Interface):
234+ """A person's view on a source package in a distribution."""
235+
236+ person = Reference(IPerson)
237+ distro_source_package = Reference(IDistributionSourcePackage)
238+ displayname = TextLine()
239+
240+
241+class IPersonDistributionSourcePackageFactory(Interface):
242+ """Creates `IPersonDistributionSourcePackage`s."""
243+
244+ def create(person, distro_source_package):
245+ """Create and return an `IPersonDistributionSourcePackage`."""
246
247=== added file 'lib/lp/registry/model/persondistributionsourcepackage.py'
248--- lib/lp/registry/model/persondistributionsourcepackage.py 1970-01-01 00:00:00 +0000
249+++ lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-09 17:43:47 +0000
250@@ -0,0 +1,39 @@
251+# Copyright 2015 Canonical Ltd. This software is licensed under the
252+# GNU Affero General Public License version 3 (see the file LICENSE).
253+
254+"""A person's view on a source package in a distribution."""
255+
256+__metaclass__ = type
257+__all__ = [
258+ 'PersonDistributionSourcePackage',
259+ ]
260+
261+from zope.interface import (
262+ classProvides,
263+ implements,
264+ )
265+
266+from lp.registry.interfaces.persondistributionsourcepackage import (
267+ IPersonDistributionSourcePackage,
268+ IPersonDistributionSourcePackageFactory,
269+ )
270+
271+
272+class PersonDistributionSourcePackage:
273+
274+ implements(IPersonDistributionSourcePackage)
275+
276+ classProvides(IPersonDistributionSourcePackageFactory)
277+
278+ def __init__(self, person, distro_source_package):
279+ self.person = person
280+ self.distro_source_package = distro_source_package
281+
282+ @staticmethod
283+ def create(person, distro_source_package):
284+ return PersonDistributionSourcePackage(person, distro_source_package)
285+
286+ @property
287+ def displayname(self):
288+ return '%s in %s' % (
289+ self.person.displayname, self.distro_source_package.displayname)
290
291=== added file 'lib/lp/registry/tests/test_persondistributionsourcepackage.py'
292--- lib/lp/registry/tests/test_persondistributionsourcepackage.py 1970-01-01 00:00:00 +0000
293+++ lib/lp/registry/tests/test_persondistributionsourcepackage.py 2015-02-09 17:43:47 +0000
294@@ -0,0 +1,42 @@
295+# Copyright 2015 Canonical Ltd. This software is licensed under the
296+# GNU Affero General Public License version 3 (see the file LICENSE).
297+
298+"""Test the Person/DistributionSourcePackage non-database class."""
299+
300+__metaclass__ = type
301+
302+from lp.registry.model.persondistributionsourcepackage import (
303+ PersonDistributionSourcePackage,
304+ )
305+from lp.services.webapp.interfaces import IBreadcrumb
306+from lp.services.webapp.publisher import canonical_url
307+from lp.testing import TestCaseWithFactory
308+from lp.testing.layers import DatabaseFunctionalLayer
309+
310+
311+class TestPersonDistributionSourcePackage(TestCaseWithFactory):
312+ """Tests for `IPersonDistributionSourcePackage`s."""
313+
314+ layer = DatabaseFunctionalLayer
315+
316+ def _makePersonDistributionSourcePackage(self):
317+ person = self.factory.makePerson()
318+ dsp = self.factory.makeDistributionSourcePackage()
319+ return PersonDistributionSourcePackage(person, dsp)
320+
321+ def test_canonical_url(self):
322+ # The canonical_url of a person DSP is
323+ # ~person/distribution/+source/sourcepackagename.
324+ pdsp = self._makePersonDistributionSourcePackage()
325+ dsp = pdsp.distro_source_package
326+ expected = 'http://launchpad.dev/~%s/%s/+source/%s' % (
327+ pdsp.person.name, dsp.distribution.name,
328+ dsp.sourcepackagename.name)
329+ self.assertEqual(expected, canonical_url(pdsp))
330+
331+ def test_breadcrumb(self):
332+ # Person DSPs give the DSP as their breadcrumb url.
333+ pdsp = self._makePersonDistributionSourcePackage()
334+ breadcrumb = IBreadcrumb(pdsp, None)
335+ self.assertEqual(
336+ canonical_url(pdsp.distro_source_package), breadcrumb.url)