Merge lp:~bac/launchpad/bug-490518-link-suggestion into lp:launchpad
- bug-490518-link-suggestion
- Merge into devel
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 |
Related bugs: |
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.
Description of the change
Brad Crittenden (bac) wrote : | # |
Curtis Hovey (sinzui) wrote : | # |
..
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
...
> @@ -294,3 +301,49 @@
> """A View to show Answers help."""
>
> page_title = 'Help and support options'
> +
> +
> +class SourcePackageAs
LaunchpadEditFo
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', LaunchpadRadioW
> + product_suggestions = None
> +
> + def setUpFields(self):
> + """See `LaunchpadFormV
> + super(SourcePac
> + # Find registered products that are similarly named to the source
> + # package.
> + product_vocab = getVocabularyRe
> + matches = product_
> + # Based upon the matching products, create a new vocabulary with
> + # term descriptions that include a link to the product.
> + self.product_
> + vocab_terms = []
> + for item in matches:
> + product = item.value
> + self.product_
> + item_url = canonical_
> + description = """<a href="%s">%s</a>""" % (
> + item_url, product.
I think this needs escaping. product.displayname may contain [?><].
> + vocab_terms.
> + upstream_vocabulary = SimpleVocabular
> +
> + self.form_fields = Fields(
> + Choice(
> + 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=
> + #description=
I suppose this is not needed.
> + required=True))
> +
> + @action('Link to Upstream Project', name='link')
> + def link(self, action, data):
> + upstream = data.get(
> + self.context.
> + self.request.
> + 'The project %s was linked to this source package.' %
> + upstream.
> + ...
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.
Brad Crittenden (bac) wrote : | # |
Curtis, based on your macro I created:
<metal:plural-msg define-
<tal:comment condition=
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=
replace=
<tal:plural
define=
condition=
replace=
</metal:plural-msg>
And invoked it like:
>
<b>
</b>
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_
What do you think?
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.
Martin Albisetti (beuno) : | # |
Preview Diff
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’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’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’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’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’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 | + or |
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> |
= 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 /dev.launchpad. net/Registry/ UbuntuLinkUpstr eam. He and I had discussions
https:/
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: /launchpad. dev/ubuntu/ hoary/+ source/ evolution
https:/
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: registry/ browser/ configure. zcml registry/ browser/ sourcepackage. py registry/ templates/ sourcepackage- index.pt registry/ browser/ tests/sourcepac kage-views. txt registry/ templates/ sourcepackage- portlet- associations. pt
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
== Pylint notices ==
lib/lp/ registry/ browser/ sourcepackage. py interface' (No module named restful)
28: [F0401] Unable to import 'lazr.restful.