Merge lp:~bac/launchpad/bug-490518-link-suggestion into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Curtis Hovey
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~bac/launchpad/bug-490518-link-suggestion
Merge into: lp:launchpad
Diff against target: 409 lines (+249/-61)
7 files modified
lib/lp/app/templates/base-layout-macros.pt (+18/-0)
lib/lp/registry/browser/configure.zcml (+7/-0)
lib/lp/registry/browser/sourcepackage.py (+58/-3)
lib/lp/registry/browser/tests/sourcepackage-views.txt (+64/-0)
lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt (+4/-3)
lib/lp/registry/templates/sourcepackage-index.pt (+1/-55)
lib/lp/registry/templates/sourcepackage-portlet-associations.pt (+97/-0)
To merge this branch: bzr merge lp:~bac/launchpad/bug-490518-link-suggestion
Reviewer Review Type Date Requested Status
Martin Albisetti (community) ui Approve
Curtis Hovey (community) code Approve
Review via email: mp+18040@code.launchpad.net

Commit message

Allow users to set upstream associations via a new portlet on the sourcepackage page.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

= Summary =

The source package page needs a portlet to display upstream information if known -or-
allow the user to set it if not known.

== Proposed fix ==

A portlet is created that creates a widget of likely Launchpad projects the user can
pick from to set the upstream for the source package. A link to go to the existing
package editing form is also provided.

== Pre-implementation notes ==

The work is based on the design proposed by Curtis at
https://dev.launchpad.net/Registry/UbuntuLinkUpstream. He and I had discussions
before and during the implementation.

== Implementation details ==

Pretty straightforward: create the portlet and DTRT.

== Tests ==

bin/test -vvt sourcepackage-views.txt

== Demo and Q/A ==

In launchpad.dev, remove the association for evolution to hoary and then view:
https://launchpad.dev/ubuntu/hoary/+source/evolution

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/sourcepackage.py
  lib/lp/registry/templates/sourcepackage-index.pt
  lib/lp/registry/browser/tests/sourcepackage-views.txt
  lib/lp/registry/templates/sourcepackage-portlet-associations.pt

== Pylint notices ==

lib/lp/registry/browser/sourcepackage.py
    28: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)

Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (8.9 KiB)

..

=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py 2009-12-18 13:25:19 +0000
+++ lib/lp/registry/browser/sourcepackage.py 2010-01-25 23:25:27 +0000

...

> @@ -294,3 +301,49 @@
> """A View to show Answers help."""
>
> page_title = 'Help and support options'
> +
> +
> +class SourcePackageAssociationPortletView(LaunchpadEditFormView):

LaunchpadEditFormView is used to update the context object, which is not
the case here; it is creating Packaging object though an indirect method.
Why was this base class used instead of LaunchpadFormView? It is because
it can be used to update the Packaging object after it is set? If so,
the view will have to be registered on IPackaging, which does not exist in
this scenario.

> + """A view for linking to an upstream package."""
> +
> + schema = Interface
> + custom_widget(
> + 'upstream', LaunchpadRadioWidget, orientation='vertical')
> + product_suggestions = None
> +
> + def setUpFields(self):
> + """See `LaunchpadFormView`."""
> + super(SourcePackageAssociationPortletView, self).setUpFields()
> + # Find registered products that are similarly named to the source
> + # package.
> + product_vocab = getVocabularyRegistry().get(None, 'Product')
> + matches = product_vocab.searchForTerms(self.context.name)
> + # Based upon the matching products, create a new vocabulary with
> + # term descriptions that include a link to the product.
> + self.product_suggestions = []
> + vocab_terms = []
> + for item in matches:
> + product = item.value
> + self.product_suggestions.append(product)
> + item_url = canonical_url(product)
> + description = """<a href="%s">%s</a>""" % (
> + item_url, product.displayname)

I think this needs escaping. product.displayname may contain [?><].

> + vocab_terms.append(SimpleTerm(product, product.name, description))
> + upstream_vocabulary = SimpleVocabulary(vocab_terms)
> +
> + self.form_fields = Fields(
> + Choice(__name__='upstream',
> + title=_('Upstream project'),

I have been pondering cases where we need to be clear that we are showing
*registered* launchpad projects. Many of the confusing-ui issues is that users
do not understand that someone must register the project for it to be used.

Does the use of *registered* provide a clue that someone may need to register
the upstream project to complete the task?

> + default=None,
> + vocabulary=upstream_vocabulary,
> + #description=_("Select the upstream project"),

I suppose this is not needed.

> + required=True))
> +
> + @action('Link to Upstream Project', name='link')
> + def link(self, action, data):
> + upstream = data.get('upstream')
> + self.context.setPackaging(upstream.development_focus, self.user)
> + self.request.response.addInfoNotification(
> + 'The project %s was linked to this source package.' %
> + upstream.displayname)
> + ...

Read more...

review: Needs Fixing (code)
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the review Curtis -- all good suggestions and all changes made except for the plural issue. I'll work with you today to extend and incorporate your macro.

Revision history for this message
Brad Crittenden (bac) wrote :

Curtis, based on your macro I created:

<metal:plural-msg define-macro="plural-message">
  <tal:comment condition="nothing">
    Expected variables to be defined in a containing tag or global:
    count - value to check to determine plural form
    singluar - string to use when count == 1
    plural - string to use when count > 1. If no plural is given it defaults
    to the singular value + 's'.
  </tal:comment>
  <tal:singular
    condition="python: count == 1"
    replace="singular" />
  <tal:plural
     define="l_default string:s;
             l_plural plural | string:$singular$l_default;"
    condition="python: count != 1"
    replace="l_plural" />
</metal:plural-msg>

And invoked it like:

          <tal:message
             define="count view/product_suggestions_count;
                     singular string:Is the following project the upstream for this source package?;
                     plural string:Is one of these projects the upstream for this source package?"
             >
            <b>
            <metal:message use-macro="context/@@+base-layout-macros/plural-message"/>
            </b>
          </tal:message>

That's all good but it is longer to use than the old way and required me to create a new property on the view class (product_suggestions_count). So, I'm left a little underwhelmed.

What do you think?

Revision history for this message
Curtis Hovey (sinzui) wrote :

Your changes look good. I am going to merge your plural marcro into my branch and use it because your implementation is more universal.

review: Approve (code)
Revision history for this message
Martin Albisetti (beuno) :
review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
2--- lib/lp/app/templates/base-layout-macros.pt 2010-01-22 15:47:53 +0000
3+++ lib/lp/app/templates/base-layout-macros.pt 2010-01-30 12:11:18 +0000
4@@ -391,4 +391,22 @@
5 </div>
6 </metal:site-message>
7
8+<metal:plural-msg define-macro="plural-message">
9+ <tal:comment condition="nothing">
10+ Expected variables to be defined in a containing tag or global:
11+ count - value to check to determine plural form
12+ singluar - string to use when count == 1
13+ plural - string to use when count > 1. If no plural is given it defaults
14+ to the singular value + 's'.
15+ </tal:comment>
16+ <tal:singular
17+ condition="python: count == 1"
18+ replace="singular" />
19+ <tal:plural
20+ define="l_default string:s;
21+ l_plural plural | string:$singular$l_default;"
22+ condition="python: count != 1"
23+ replace="l_plural" />
24+</metal:plural-msg>
25+
26 </macros>
27
28=== modified file 'lib/lp/registry/browser/configure.zcml'
29--- lib/lp/registry/browser/configure.zcml 2010-01-21 23:46:24 +0000
30+++ lib/lp/registry/browser/configure.zcml 2010-01-30 12:11:18 +0000
31@@ -1937,6 +1937,13 @@
32 <browser:page
33 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
34 permission="zope.Public"
35+ name="+portlet-associations"
36+ facet="overview"
37+ class="lp.registry.browser.sourcepackage.SourcePackageAssociationPortletView"
38+ template="../templates/sourcepackage-portlet-associations.pt"/>
39+ <browser:page
40+ for="lp.registry.interfaces.sourcepackage.ISourcePackage"
41+ permission="zope.Public"
42 class="lp.registry.browser.sourcepackage.SourcePackageHelpView"
43 name="+gethelp"
44 facet="answers"
45
46=== modified file 'lib/lp/registry/browser/sourcepackage.py'
47--- lib/lp/registry/browser/sourcepackage.py 2009-12-18 13:25:19 +0000
48+++ lib/lp/registry/browser/sourcepackage.py 2010-01-30 12:11:18 +0000
49@@ -6,6 +6,7 @@
50 __metaclass__ = type
51
52 __all__ = [
53+ 'SourcePackageAssociationPortletView',
54 'SourcePackageBreadcrumb',
55 'SourcePackageChangeUpstreamView',
56 'SourcePackageFacets',
57@@ -16,12 +17,20 @@
58 ]
59
60 from apt_pkg import ParseSrcDepends
61+from cgi import escape
62 from zope.component import getUtility, getMultiAdapter
63 from zope.app.form.interfaces import IInputWidget
64-from zope.formlib.form import FormFields
65+from zope.formlib.form import Fields, FormFields
66+from zope.interface import Interface
67+from zope.schema import Choice
68+from zope.schema.vocabulary import (
69+ getVocabularyRegistry, SimpleVocabulary, SimpleTerm)
70
71 from lazr.restful.interface import copy_field
72
73+from canonical.cachedproperty import cachedproperty
74+from canonical.widgets import LaunchpadRadioWidget
75+
76 from canonical.launchpad import helpers
77 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
78 from canonical.launchpad.browser.packagerelationship import (
79@@ -35,8 +44,9 @@
80 from lp.translations.interfaces.potemplate import IPOTemplateSet
81 from canonical.launchpad import _
82 from canonical.launchpad.webapp import (
83- action, ApplicationMenu, GetitemNavigation, LaunchpadEditFormView, Link,
84- redirection, StandardLaunchpadFacets, stepto)
85+ action, ApplicationMenu, custom_widget, GetitemNavigation,
86+ LaunchpadEditFormView, LaunchpadFormView, Link, redirection,
87+ StandardLaunchpadFacets, stepto)
88 from canonical.launchpad.webapp import canonical_url
89 from canonical.launchpad.webapp.authorization import check_permission
90 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
91@@ -294,3 +304,48 @@
92 """A View to show Answers help."""
93
94 page_title = 'Help and support options'
95+
96+
97+class SourcePackageAssociationPortletView(LaunchpadFormView):
98+ """A view for linking to an upstream package."""
99+
100+ schema = Interface
101+ custom_widget(
102+ 'upstream', LaunchpadRadioWidget, orientation='vertical')
103+ product_suggestions = None
104+
105+ def setUpFields(self):
106+ """See `LaunchpadFormView`."""
107+ super(SourcePackageAssociationPortletView, self).setUpFields()
108+ # Find registered products that are similarly named to the source
109+ # package.
110+ product_vocab = getVocabularyRegistry().get(None, 'Product')
111+ matches = product_vocab.searchForTerms(self.context.name)
112+ # Based upon the matching products, create a new vocabulary with
113+ # term descriptions that include a link to the product.
114+ self.product_suggestions = []
115+ vocab_terms = []
116+ for item in matches:
117+ product = item.value
118+ self.product_suggestions.append(product)
119+ item_url = canonical_url(product)
120+ description = """<a href="%s">%s</a>""" % (
121+ item_url, escape(product.displayname))
122+ vocab_terms.append(SimpleTerm(product, product.name, description))
123+ upstream_vocabulary = SimpleVocabulary(vocab_terms)
124+
125+ self.form_fields = Fields(
126+ Choice(__name__='upstream',
127+ title=_('Registered upstream project'),
128+ default=None,
129+ vocabulary=upstream_vocabulary,
130+ required=True))
131+
132+ @action('Link to Upstream Project', name='link')
133+ def link(self, action, data):
134+ upstream = data.get('upstream')
135+ self.context.setPackaging(upstream.development_focus, self.user)
136+ self.request.response.addInfoNotification(
137+ 'The project %s was linked to this source package.' %
138+ upstream.displayname)
139+ self.next_url = self.request.getURL()
140
141=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
142--- lib/lp/registry/browser/tests/sourcepackage-views.txt 2009-10-27 04:04:55 +0000
143+++ lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-01-30 12:11:18 +0000
144@@ -1,6 +1,8 @@
145 SourcePackage views
146 ===================
147
148+Edit packaging view
149+-------------------
150
151 >>> product = factory.makeProduct(name='bonkers', displayname='Bonkers')
152 >>> productseries = factory.makeProductSeries(
153@@ -96,3 +98,65 @@
154
155 >>> print view.request.response.notifications
156 []
157+
158+Upstream associations portlet
159+-----------------------------
160+
161+The upstreams associations portlet either displays the upstream
162+information if it is already set or gives the user the opportunity to
163+suggest the association. The suggestion is based on a
164+ProductVocabulary query using the source package name.
165+
166+Since the bonkers source project was associated previously with the
167+bonkers project, the portlet will display that information.
168+
169+ >>> view = create_initialized_view(package, name='+portlet-associations')
170+ >>> for product in view.product_suggestions:
171+ ... print product.name
172+ bonkers
173+
174+ >>> from canonical.launchpad.testing.pages import (
175+ ... extract_text, find_tag_by_id)
176+ >>> content = extract_text(find_tag_by_id(view.render(), 'upstreams'))
177+ >>> print content
178+ Project:...Bonkers...
179+ Series:...crazy...
180+
181+A new source project that is not linked to an upstream will result in
182+the portlet showing the suggested project.
183+
184+ >>> product = factory.makeProduct(name='lernid', displayname='Lernid')
185+ >>> sourcepackagename = factory.makeSourcePackageName(name='lernid')
186+ >>> package = factory.makeSourcePackage(
187+ ... sourcepackagename=sourcepackagename, distroseries=distroseries)
188+
189+ >>> view = create_initialized_view(package, name='+portlet-associations')
190+ >>> for product in view.product_suggestions:
191+ ... print product.name
192+ lernid
193+
194+ >>> content = extract_text(find_tag_by_id(view.render(), 'no-upstreams'))
195+ >>> print content
196+ Launchpad doesn&#8217;t know which project and series this
197+ package belongs to. ...
198+ Is the following project the upstream for this source package?
199+ Registered upstream project:
200+ Lernid...
201+
202+If there are multiple potential matches they are all shown.
203+
204+ >>> product = factory.makeProduct(name='lernid-dev', displayname='Lernid Dev')
205+ >>> view = create_initialized_view(package, name='+portlet-associations')
206+ >>> for product in view.product_suggestions:
207+ ... print product.name
208+ lernid
209+ lernid-dev
210+
211+ >>> content = extract_text(find_tag_by_id(view.render(), 'no-upstreams'))
212+ >>> print content
213+ Launchpad doesn&#8217;t know which project and series this
214+ package belongs to. ...
215+ Is one of these projects the upstream for this source package?
216+ Registered upstream project:
217+ Lernid...
218+ Lernid Dev...
219
220=== modified file 'lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt'
221--- lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2009-12-08 05:02:32 +0000
222+++ lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-01-30 12:11:18 +0000
223@@ -15,13 +15,14 @@
224 >>> user_browser.getLink('pmount').click()
225 >>> print extract_text(find_tag_by_id(
226 ... user_browser.contents, 'no-upstreams'))
227- Launchpad doesn&#8217;t know which project and series this package
228- belongs to. ... Can you set the upstream project in Launchpad?
229+ There are no projects registered in Launchpad that are a potential
230+ match for this source package. Can you help us find one?
231+ Set upstream link
232
233 No Privileges Person knows that the pmount package comes from the thunderbird
234 project. He sets the upstream packaging link and sees that it is set.
235
236- >>> user_browser.getLink('set the upstream project in Launchpad').click()
237+ >>> user_browser.getLink('Set upstream link').click()
238 >>> user_browser.getControl(
239 ... name="field.productseries").value = 'thunderbird/trunk'
240 >>> user_browser.getControl("Change").click()
241
242=== modified file 'lib/lp/registry/templates/sourcepackage-index.pt'
243--- lib/lp/registry/templates/sourcepackage-index.pt 2009-12-03 16:54:29 +0000
244+++ lib/lp/registry/templates/sourcepackage-index.pt 2010-01-30 12:11:18 +0000
245@@ -55,61 +55,7 @@
246 </div>
247
248 <div class="yui-u">
249- <div class="portlet"
250- tal:define="series context/productseries">
251- <h2>Upstream associations</h2>
252-
253- <tal:has_series condition="series">
254- <div id="upstreams" class="two-column-list">
255- <dl
256- tal:define="project series/product/project"
257- tal:condition="project">
258- <dt>Project Group:</dt>
259- <dd>
260- <a tal:replace="structure project/fmt:link" />
261- </dd>
262- </dl>
263-
264- <dl>
265- <dt>Project:</dt>
266- <dd>
267- <a tal:replace="structure series/product/fmt:link" />
268- </dd>
269- </dl>
270-
271- <dl>
272- <dt>Series:</dt>
273- <dd>
274- <a
275- tal:content="series/name"
276- tal:attributes="href series/fmt:url">Series</a>
277- <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
278- </dd>
279- </dl>
280- </div>
281-
282- <ul class="horizontal">
283- <li>
284- <a
285- tal:attributes="href series/product/menu:overview/packages/fmt:url"
286- >Show upstream links</a>
287- </li>
288- </ul>
289- </tal:has_series>
290-
291- <tal:has_no_series condition="not: series">
292- <p id="no-upstreams">
293- Launchpad doesn&#8217;t know which project and series this
294- package belongs to. Links from distribution packages to
295- upstream project let distribution and upstream
296- maintainers share bugs, patches, and translations
297- efficiently. Can you
298- <a tal:attributes="
299- href context/menu:overview/edit_packaging/url">set
300- the upstream project in Launchpad</a>?
301- </p>
302- </tal:has_no_series>
303- </div>
304+ <div class="portlet" tal:content="structure context/@@+portlet-associations" />
305 </div>
306 </div>
307
308
309=== added file 'lib/lp/registry/templates/sourcepackage-portlet-associations.pt'
310--- lib/lp/registry/templates/sourcepackage-portlet-associations.pt 1970-01-01 00:00:00 +0000
311+++ lib/lp/registry/templates/sourcepackage-portlet-associations.pt 2010-01-30 12:11:18 +0000
312@@ -0,0 +1,97 @@
313+<tal:root
314+ xmlns:tal="http://xml.zope.org/namespaces/tal"
315+ xmlns:metal="http://xml.zope.org/namespaces/metal"
316+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
317+ omit-tag="">
318+
319+ <div id="portlet-associations">
320+ <h2>Upstream associations</h2>
321+
322+ <div class="portletBody portletContent"
323+ tal:define="series context/productseries">
324+ <tal:has_series condition="series">
325+ <div id="upstreams" class="two-column-list">
326+ <dl
327+ tal:define="project series/product/project"
328+ tal:condition="project">
329+ <dt>Project Group:</dt>
330+ <dd>
331+ <a tal:replace="structure project/fmt:link" />
332+ </dd>
333+ </dl>
334+
335+ <dl>
336+ <dt>Project:</dt>
337+ <dd>
338+ <a tal:replace="structure series/product/fmt:link" />
339+ </dd>
340+ </dl>
341+
342+ <dl>
343+ <dt>Series:</dt>
344+ <dd>
345+ <a
346+ tal:content="series/name"
347+ tal:attributes="href series/fmt:url">Series</a>
348+ <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
349+ </dd>
350+ </dl>
351+ </div>
352+
353+ <ul class="horizontal">
354+ <li>
355+ <a
356+ tal:attributes="href series/product/menu:overview/packages/fmt:url"
357+ >Show upstream links</a>
358+ </li>
359+ </ul>
360+ </tal:has_series>
361+
362+ <tal:has_no_series condition="not: series">
363+ <div id="no-upstreams" tal:condition="view/product_suggestions">
364+ <p>
365+ Launchpad doesn&#8217;t know which project and series this
366+ package belongs to. Links from distribution packages to
367+ upstream project let distribution and upstream
368+ maintainers share bugs, patches, and translations
369+ efficiently.
370+ </p>
371+
372+ <tal:message
373+ define="count python:len(view.product_suggestions);
374+ singular string:Is the following project the upstream for this source package?;
375+ plural string:Is one of these projects the upstream for this source package?"
376+ >
377+ <b>
378+ <metal:message use-macro="context/@@+base-layout-macros/plural-message"/>
379+ </b>
380+ </tal:message>
381+
382+ <div tal:condition="python: len(view.product_suggestions) > 1">
383+ <b>Is one of these projects the upstream for this source
384+ package?</b>
385+ </div>
386+ <div tal:condition="python: len(view.product_suggestions) == 1">
387+ <b>Is the following project the upstream for this source package?</b>
388+ </div>
389+ <div metal:use-macro="context/@@launchpad_form/form">
390+ <div class="actions" metal:fill-slot="buttons">
391+ <input tal:replace="structure view/link/render"/>
392+ &nbsp;or&nbsp;
393+ <a tal:replace="structure
394+ context/menu:overview/set_upstream/fmt:link" />
395+ </div>
396+ </div>
397+ </div>
398+ <div id="no-upstreams" tal:condition="not: view/product_suggestions">
399+ <p>
400+ There are no projects registered in Launchpad that are a potential
401+ match for this source package. Can you help us find one?
402+ </p>
403+ <a tal:replace="structure
404+ context/menu:overview/set_upstream/fmt:link" />
405+ </div>
406+ </tal:has_no_series>
407+ </div>
408+ </div>
409+</tal:root>