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
=== modified file 'lib/lp/app/templates/base-layout-macros.pt'
--- lib/lp/app/templates/base-layout-macros.pt 2010-01-22 15:47:53 +0000
+++ lib/lp/app/templates/base-layout-macros.pt 2010-01-30 12:11:18 +0000
@@ -391,4 +391,22 @@
391 </div>391 </div>
392</metal:site-message>392</metal:site-message>
393393
394<metal:plural-msg define-macro="plural-message">
395 <tal:comment condition="nothing">
396 Expected variables to be defined in a containing tag or global:
397 count - value to check to determine plural form
398 singluar - string to use when count == 1
399 plural - string to use when count > 1. If no plural is given it defaults
400 to the singular value + 's'.
401 </tal:comment>
402 <tal:singular
403 condition="python: count == 1"
404 replace="singular" />
405 <tal:plural
406 define="l_default string:s;
407 l_plural plural | string:$singular$l_default;"
408 condition="python: count != 1"
409 replace="l_plural" />
410</metal:plural-msg>
411
394</macros>412</macros>
395413
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-01-21 23:46:24 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-01-30 12:11:18 +0000
@@ -1937,6 +1937,13 @@
1937 <browser:page1937 <browser:page
1938 for="lp.registry.interfaces.sourcepackage.ISourcePackage"1938 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
1939 permission="zope.Public"1939 permission="zope.Public"
1940 name="+portlet-associations"
1941 facet="overview"
1942 class="lp.registry.browser.sourcepackage.SourcePackageAssociationPortletView"
1943 template="../templates/sourcepackage-portlet-associations.pt"/>
1944 <browser:page
1945 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
1946 permission="zope.Public"
1940 class="lp.registry.browser.sourcepackage.SourcePackageHelpView"1947 class="lp.registry.browser.sourcepackage.SourcePackageHelpView"
1941 name="+gethelp"1948 name="+gethelp"
1942 facet="answers"1949 facet="answers"
19431950
=== 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-30 12:11:18 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'SourcePackageAssociationPortletView',
9 'SourcePackageBreadcrumb',10 'SourcePackageBreadcrumb',
10 'SourcePackageChangeUpstreamView',11 'SourcePackageChangeUpstreamView',
11 'SourcePackageFacets',12 'SourcePackageFacets',
@@ -16,12 +17,20 @@
16 ]17 ]
1718
18from apt_pkg import ParseSrcDepends19from apt_pkg import ParseSrcDepends
20from cgi import escape
19from zope.component import getUtility, getMultiAdapter21from zope.component import getUtility, getMultiAdapter
20from zope.app.form.interfaces import IInputWidget22from zope.app.form.interfaces import IInputWidget
21from zope.formlib.form import FormFields23from zope.formlib.form import Fields, FormFields
24from zope.interface import Interface
25from zope.schema import Choice
26from zope.schema.vocabulary import (
27 getVocabularyRegistry, SimpleVocabulary, SimpleTerm)
2228
23from lazr.restful.interface import copy_field29from lazr.restful.interface import copy_field
2430
31from canonical.cachedproperty import cachedproperty
32from canonical.widgets import LaunchpadRadioWidget
33
25from canonical.launchpad import helpers34from canonical.launchpad import helpers
26from lp.bugs.browser.bugtask import BugTargetTraversalMixin35from lp.bugs.browser.bugtask import BugTargetTraversalMixin
27from canonical.launchpad.browser.packagerelationship import (36from canonical.launchpad.browser.packagerelationship import (
@@ -35,8 +44,9 @@
35from lp.translations.interfaces.potemplate import IPOTemplateSet44from lp.translations.interfaces.potemplate import IPOTemplateSet
36from canonical.launchpad import _45from canonical.launchpad import _
37from canonical.launchpad.webapp import (46from canonical.launchpad.webapp import (
38 action, ApplicationMenu, GetitemNavigation, LaunchpadEditFormView, Link,47 action, ApplicationMenu, custom_widget, GetitemNavigation,
39 redirection, StandardLaunchpadFacets, stepto)48 LaunchpadEditFormView, LaunchpadFormView, Link, redirection,
49 StandardLaunchpadFacets, stepto)
40from canonical.launchpad.webapp import canonical_url50from canonical.launchpad.webapp import canonical_url
41from canonical.launchpad.webapp.authorization import check_permission51from canonical.launchpad.webapp.authorization import check_permission
42from canonical.launchpad.webapp.breadcrumb import Breadcrumb52from canonical.launchpad.webapp.breadcrumb import Breadcrumb
@@ -294,3 +304,48 @@
294 """A View to show Answers help."""304 """A View to show Answers help."""
295305
296 page_title = 'Help and support options'306 page_title = 'Help and support options'
307
308
309class SourcePackageAssociationPortletView(LaunchpadFormView):
310 """A view for linking to an upstream package."""
311
312 schema = Interface
313 custom_widget(
314 'upstream', LaunchpadRadioWidget, orientation='vertical')
315 product_suggestions = None
316
317 def setUpFields(self):
318 """See `LaunchpadFormView`."""
319 super(SourcePackageAssociationPortletView, self).setUpFields()
320 # Find registered products that are similarly named to the source
321 # package.
322 product_vocab = getVocabularyRegistry().get(None, 'Product')
323 matches = product_vocab.searchForTerms(self.context.name)
324 # Based upon the matching products, create a new vocabulary with
325 # term descriptions that include a link to the product.
326 self.product_suggestions = []
327 vocab_terms = []
328 for item in matches:
329 product = item.value
330 self.product_suggestions.append(product)
331 item_url = canonical_url(product)
332 description = """<a href="%s">%s</a>""" % (
333 item_url, escape(product.displayname))
334 vocab_terms.append(SimpleTerm(product, product.name, description))
335 upstream_vocabulary = SimpleVocabulary(vocab_terms)
336
337 self.form_fields = Fields(
338 Choice(__name__='upstream',
339 title=_('Registered upstream project'),
340 default=None,
341 vocabulary=upstream_vocabulary,
342 required=True))
343
344 @action('Link to Upstream Project', name='link')
345 def link(self, action, data):
346 upstream = data.get('upstream')
347 self.context.setPackaging(upstream.development_focus, self.user)
348 self.request.response.addInfoNotification(
349 'The project %s was linked to this source package.' %
350 upstream.displayname)
351 self.next_url = self.request.getURL()
297352
=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
--- lib/lp/registry/browser/tests/sourcepackage-views.txt 2009-10-27 04:04:55 +0000
+++ lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-01-30 12:11:18 +0000
@@ -1,6 +1,8 @@
1SourcePackage views1SourcePackage views
2===================2===================
33
4Edit packaging view
5-------------------
46
5 >>> product = factory.makeProduct(name='bonkers', displayname='Bonkers')7 >>> product = factory.makeProduct(name='bonkers', displayname='Bonkers')
6 >>> productseries = factory.makeProductSeries(8 >>> productseries = factory.makeProductSeries(
@@ -96,3 +98,65 @@
9698
97 >>> print view.request.response.notifications99 >>> print view.request.response.notifications
98 []100 []
101
102Upstream associations portlet
103-----------------------------
104
105The upstreams associations portlet either displays the upstream
106information if it is already set or gives the user the opportunity to
107suggest the association. The suggestion is based on a
108ProductVocabulary query using the source package name.
109
110Since the bonkers source project was associated previously with the
111bonkers project, the portlet will display that information.
112
113 >>> view = create_initialized_view(package, name='+portlet-associations')
114 >>> for product in view.product_suggestions:
115 ... print product.name
116 bonkers
117
118 >>> from canonical.launchpad.testing.pages import (
119 ... extract_text, find_tag_by_id)
120 >>> content = extract_text(find_tag_by_id(view.render(), 'upstreams'))
121 >>> print content
122 Project:...Bonkers...
123 Series:...crazy...
124
125A new source project that is not linked to an upstream will result in
126the portlet showing the suggested project.
127
128 >>> product = factory.makeProduct(name='lernid', displayname='Lernid')
129 >>> sourcepackagename = factory.makeSourcePackageName(name='lernid')
130 >>> package = factory.makeSourcePackage(
131 ... sourcepackagename=sourcepackagename, distroseries=distroseries)
132
133 >>> view = create_initialized_view(package, name='+portlet-associations')
134 >>> for product in view.product_suggestions:
135 ... print product.name
136 lernid
137
138 >>> content = extract_text(find_tag_by_id(view.render(), 'no-upstreams'))
139 >>> print content
140 Launchpad doesn&#8217;t know which project and series this
141 package belongs to. ...
142 Is the following project the upstream for this source package?
143 Registered upstream project:
144 Lernid...
145
146If there are multiple potential matches they are all shown.
147
148 >>> product = factory.makeProduct(name='lernid-dev', displayname='Lernid Dev')
149 >>> view = create_initialized_view(package, name='+portlet-associations')
150 >>> for product in view.product_suggestions:
151 ... print product.name
152 lernid
153 lernid-dev
154
155 >>> content = extract_text(find_tag_by_id(view.render(), 'no-upstreams'))
156 >>> print content
157 Launchpad doesn&#8217;t know which project and series this
158 package belongs to. ...
159 Is one of these projects the upstream for this source package?
160 Registered upstream project:
161 Lernid...
162 Lernid Dev...
99163
=== modified file 'lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt'
--- lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2009-12-08 05:02:32 +0000
+++ lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-01-30 12:11:18 +0000
@@ -15,13 +15,14 @@
15 >>> user_browser.getLink('pmount').click()15 >>> user_browser.getLink('pmount').click()
16 >>> print extract_text(find_tag_by_id(16 >>> print extract_text(find_tag_by_id(
17 ... user_browser.contents, 'no-upstreams'))17 ... user_browser.contents, 'no-upstreams'))
18 Launchpad doesn&#8217;t know which project and series this package18 There are no projects registered in Launchpad that are a potential
19 belongs to. ... Can you set the upstream project in Launchpad?19 match for this source package. Can you help us find one?
20 Set upstream link
2021
21No Privileges Person knows that the pmount package comes from the thunderbird22No Privileges Person knows that the pmount package comes from the thunderbird
22project. He sets the upstream packaging link and sees that it is set.23project. He sets the upstream packaging link and sees that it is set.
2324
24 >>> user_browser.getLink('set the upstream project in Launchpad').click()25 >>> user_browser.getLink('Set upstream link').click()
25 >>> user_browser.getControl(26 >>> user_browser.getControl(
26 ... name="field.productseries").value = 'thunderbird/trunk'27 ... name="field.productseries").value = 'thunderbird/trunk'
27 >>> user_browser.getControl("Change").click()28 >>> user_browser.getControl("Change").click()
2829
=== modified file 'lib/lp/registry/templates/sourcepackage-index.pt'
--- lib/lp/registry/templates/sourcepackage-index.pt 2009-12-03 16:54:29 +0000
+++ lib/lp/registry/templates/sourcepackage-index.pt 2010-01-30 12:11:18 +0000
@@ -55,61 +55,7 @@
55 </div>55 </div>
5656
57 <div class="yui-u">57 <div class="yui-u">
58 <div class="portlet"58 <div class="portlet" tal:content="structure context/@@+portlet-associations" />
59 tal:define="series context/productseries">
60 <h2>Upstream associations</h2>
61
62 <tal:has_series condition="series">
63 <div id="upstreams" class="two-column-list">
64 <dl
65 tal:define="project series/product/project"
66 tal:condition="project">
67 <dt>Project Group:</dt>
68 <dd>
69 <a tal:replace="structure project/fmt:link" />
70 </dd>
71 </dl>
72
73 <dl>
74 <dt>Project:</dt>
75 <dd>
76 <a tal:replace="structure series/product/fmt:link" />
77 </dd>
78 </dl>
79
80 <dl>
81 <dt>Series:</dt>
82 <dd>
83 <a
84 tal:content="series/name"
85 tal:attributes="href series/fmt:url">Series</a>
86 <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
87 </dd>
88 </dl>
89 </div>
90
91 <ul class="horizontal">
92 <li>
93 <a
94 tal:attributes="href series/product/menu:overview/packages/fmt:url"
95 >Show upstream links</a>
96 </li>
97 </ul>
98 </tal:has_series>
99
100 <tal:has_no_series condition="not: series">
101 <p id="no-upstreams">
102 Launchpad doesn&#8217;t know which project and series this
103 package belongs to. Links from distribution packages to
104 upstream project let distribution and upstream
105 maintainers share bugs, patches, and translations
106 efficiently. Can you
107 <a tal:attributes="
108 href context/menu:overview/edit_packaging/url">set
109 the upstream project in Launchpad</a>?
110 </p>
111 </tal:has_no_series>
112 </div>
113 </div>59 </div>
114 </div>60 </div>
11561
11662
=== added file 'lib/lp/registry/templates/sourcepackage-portlet-associations.pt'
--- lib/lp/registry/templates/sourcepackage-portlet-associations.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/sourcepackage-portlet-associations.pt 2010-01-30 12:11:18 +0000
@@ -0,0 +1,97 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7 <div id="portlet-associations">
8 <h2>Upstream associations</h2>
9
10 <div class="portletBody portletContent"
11 tal:define="series context/productseries">
12 <tal:has_series condition="series">
13 <div id="upstreams" class="two-column-list">
14 <dl
15 tal:define="project series/product/project"
16 tal:condition="project">
17 <dt>Project Group:</dt>
18 <dd>
19 <a tal:replace="structure project/fmt:link" />
20 </dd>
21 </dl>
22
23 <dl>
24 <dt>Project:</dt>
25 <dd>
26 <a tal:replace="structure series/product/fmt:link" />
27 </dd>
28 </dl>
29
30 <dl>
31 <dt>Series:</dt>
32 <dd>
33 <a
34 tal:content="series/name"
35 tal:attributes="href series/fmt:url">Series</a>
36 <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
37 </dd>
38 </dl>
39 </div>
40
41 <ul class="horizontal">
42 <li>
43 <a
44 tal:attributes="href series/product/menu:overview/packages/fmt:url"
45 >Show upstream links</a>
46 </li>
47 </ul>
48 </tal:has_series>
49
50 <tal:has_no_series condition="not: series">
51 <div id="no-upstreams" tal:condition="view/product_suggestions">
52 <p>
53 Launchpad doesn&#8217;t know which project and series this
54 package belongs to. Links from distribution packages to
55 upstream project let distribution and upstream
56 maintainers share bugs, patches, and translations
57 efficiently.
58 </p>
59
60 <tal:message
61 define="count python:len(view.product_suggestions);
62 singular string:Is the following project the upstream for this source package?;
63 plural string:Is one of these projects the upstream for this source package?"
64 >
65 <b>
66 <metal:message use-macro="context/@@+base-layout-macros/plural-message"/>
67 </b>
68 </tal:message>
69
70 <div tal:condition="python: len(view.product_suggestions) > 1">
71 <b>Is one of these projects the upstream for this source
72 package?</b>
73 </div>
74 <div tal:condition="python: len(view.product_suggestions) == 1">
75 <b>Is the following project the upstream for this source package?</b>
76 </div>
77 <div metal:use-macro="context/@@launchpad_form/form">
78 <div class="actions" metal:fill-slot="buttons">
79 <input tal:replace="structure view/link/render"/>
80 &nbsp;or&nbsp;
81 <a tal:replace="structure
82 context/menu:overview/set_upstream/fmt:link" />
83 </div>
84 </div>
85 </div>
86 <div id="no-upstreams" tal:condition="not: view/product_suggestions">
87 <p>
88 There are no projects registered in Launchpad that are a potential
89 match for this source package. Can you help us find one?
90 </p>
91 <a tal:replace="structure
92 context/menu:overview/set_upstream/fmt:link" />
93 </div>
94 </tal:has_no_series>
95 </div>
96 </div>
97</tal:root>