Merge lp://qastaging/~sinzui/launchpad/blueprints-ui into lp://qastaging/launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp://qastaging/~sinzui/launchpad/blueprints-ui
Merge into: lp://qastaging/launchpad
Diff against target: 1065 lines (+307/-189)
28 files modified
lib/canonical/launchpad/pagetitles.py (+0/-2)
lib/lp/blueprints/browser/configure.zcml (+2/-0)
lib/lp/blueprints/browser/specification.py (+44/-48)
lib/lp/blueprints/browser/specificationtarget.py (+32/-12)
lib/lp/blueprints/browser/sprint.py (+16/-3)
lib/lp/blueprints/browser/tests/test_menus.py (+40/-0)
lib/lp/blueprints/browser/tests/test_specificationtarget.py (+93/-0)
lib/lp/blueprints/stories/blueprints/01-creation.txt (+5/-32)
lib/lp/blueprints/stories/blueprints/02-buglinks.txt (+1/-1)
lib/lp/blueprints/stories/sprints/05-sprint-creation.txt (+2/-1)
lib/lp/blueprints/stories/standalone/subscribing.txt (+2/-2)
lib/lp/blueprints/templates/hasspecifications-specs.pt (+14/-19)
lib/lp/blueprints/templates/specification-index.pt (+1/-13)
lib/lp/blueprints/templates/specificationtarget-documentation.pt (+3/-4)
lib/lp/blueprints/templates/sprint-portlet-attendees.pt (+1/-1)
lib/lp/blueprints/templates/sprint-register.pt (+4/-0)
lib/lp/bugs/browser/buglinktarget.py (+7/-16)
lib/lp/bugs/browser/tests/buglinktarget-views.txt (+20/-4)
lib/lp/coop/answersbugs/stories/question-buglink.txt (+1/-1)
lib/lp/registry/browser/distribution.py (+1/-1)
lib/lp/registry/browser/distroseries.py (+3/-1)
lib/lp/registry/browser/person.py (+1/-0)
lib/lp/registry/browser/product.py (+1/-1)
lib/lp/registry/browser/productseries.py (+3/-1)
lib/lp/registry/browser/project.py (+1/-1)
lib/lp/registry/browser/tests/product-menus.txt (+3/-23)
lib/lp/testing/factory.py (+3/-2)
lib/lp/testing/menu.py (+3/-0)
To merge this branch: bzr merge lp://qastaging/~sinzui/launchpad/blueprints-ui
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Martin Albisetti ui Pending
Review via email: mp+14639@code.qastaging.launchpad.net
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (7.2 KiB)

This is my branch to fix 3.0 UI inconsistencies in blueprints.

    lp:~sinzui/launchpad/blueprints-ui
    Diff size: 753
    Launchpad bug: https://bugs.launchpad.net/bugs/436249
                   https://bugs.launchpad.net/bugs/456192
                   https://bugs.launchpad.net/bugs/456974
                   https://bugs.launchpad.net/bugs/422693
    Test command: ./bin/test -vv -m lp.blueprints -t buglinktarget-views
    Pre-implementation: no one
    Target release: 3.1.11

= Fix 3.0 UI inconsistencies in blueprints =

Bug 436249 [specification-index displays an empty global menu]
    When I do not have permission to edit a blueprint the box still renders.

Bug 456192 [Page that links blueprint to bug report says "question" instead]
    When I go to a blueprint page and from there click on to linking a bug
    report. The heading and title for this page say "Link question to a bug
    report" and "Link question #12345 to a bug report," respectively.

    blueprint and questions share the same view for linking to bugs. The view
    needs to be smarter by testing he context type, or the views can be
    subclassed.

Bug 456974 [the 1.0 register a blueprint button is showing on a 3.0 page]
    The page should use the involvement menu instead.

Bug 422693 [Register a sprint is missing from /sprints]
    The sprints top-level collection page needs a link to register a sprint.

== Rules ==

Bug 436249 [specification-index displays an empty global menu]
    * Replace the hard coded menu with a navigation menu.

Bug 456192 [Page that links blueprint to bug report says "question" instead]
    * Simplify the page_title because the the view is used by many objects,
      and the context object is already listed in the breadcrumbs and title.

Bug 456974 [the 1.0 register a blueprint button is showing on a 3.0 page]
    * Add an involvement menu to the pages that use the old buttons.

Bug 422693 [Register a sprint is missing from /sprints]
    * Add a Register meeting link to the menu.

== QA ==

Bug 436249 [specification-index displays an empty global menu]
    * Visit a specification that you cannot edit.
    * Verify that an empty global menu is not displayed.

Bug 456192 [Page that links blueprint to bug report says "question" instead]
    * Choose the Link to bug link.
    * Verify the heading does not not mention 'question'.

Bug 456974 [the 1.0 register a blueprint button is showing on a 3.0 page]
    * Visit the front page for a project's blueprints.
    * Verify that the 1.0 button is not shown.
    * Verify that the page has an involvement menu to register a blueprint.

Bug 422693 [Register a sprint is missing from /sprints]
    * Visit /sprints
    * Verify there is a link to create a meeting.

== Lint ==

Linting changed files:
  lib/canonical/launchpad/pagetitles.py
  lib/lp/blueprints/browser/configure.zcml
  lib/lp/blueprints/browser/specification.py
  lib/lp/blueprints/browser/specificationtarget.py
  lib/lp/blueprints/browser/sprint.py
  lib/lp/blueprints/browser/tests/test_menus.py
  lib/lp/blueprints/browser/tests/test_specificationtarget.py
  lib/lp/blueprints/stories/blueprints/01-creation.txt
  lib/lp/blueprints/stories/bl...

Read more...

Revision history for this message
Graham Binns (gmb) wrote :

Code looks good to me. You should get a UI review too, though.

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

This branch fixes the /sprints menus:
    http://people.canonical.com/~curtis/meeting-menu.png

This branch replaces the 1.0 register a sprint button with a involvement-style
porlet as is done on the bugs pages
    http://people.canonical.com/~curtis/register-a-blueprint.png

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/pagetitles.py'
2--- lib/canonical/launchpad/pagetitles.py 2009-10-16 13:20:48 +0000
3+++ lib/canonical/launchpad/pagetitles.py 2009-11-14 04:37:12 +0000
4@@ -800,8 +800,6 @@
5
6 sprint_edit = ContextTitle(smartquote('Edit "%s" details'))
7
8-sprint_index = ContextTitle('%s (sprint or meeting)')
9-
10 sprint_new = 'Register a meeting or sprint in Launchpad'
11
12 sprint_specs = ContextTitle('Blueprints for %s')
13
14=== modified file 'lib/lp/blueprints/browser/configure.zcml'
15--- lib/lp/blueprints/browser/configure.zcml 2009-09-22 19:52:18 +0000
16+++ lib/lp/blueprints/browser/configure.zcml 2009-11-14 04:37:13 +0000
17@@ -110,6 +110,7 @@
18 <browser:page
19 name="+addspec"
20 for="lp.blueprints.interfaces.sprint.ISprint"
21+ facet="specifications"
22 class="lp.blueprints.browser.specification.NewSpecificationFromSprintView"
23 permission="launchpad.AnyPerson"
24 template="../../app/templates/generic-edit.pt"/>
25@@ -235,6 +236,7 @@
26 <browser:menus
27 module="lp.blueprints.browser.specification"
28 classes="
29+ SpecificationActionMenu
30 SpecificationContextMenu"/>
31 <browser:navigation
32 module="lp.blueprints.browser.specification"
33
34=== modified file 'lib/lp/blueprints/browser/specification.py'
35--- lib/lp/blueprints/browser/specification.py 2009-09-22 16:42:19 +0000
36+++ lib/lp/blueprints/browser/specification.py 2009-11-14 04:37:13 +0000
37@@ -13,6 +13,7 @@
38 'NewSpecificationFromProjectView',
39 'NewSpecificationFromRootView',
40 'NewSpecificationFromSprintView',
41+ 'SpecificationActionMenu',
42 'SpecificationContextMenu',
43 'SpecificationNavigation',
44 'SpecificationView',
45@@ -71,12 +72,12 @@
46 HasSpecificationsView)
47
48 from canonical.launchpad.webapp import (
49- ContextMenu, LaunchpadView, LaunchpadEditFormView, LaunchpadFormView,
50- Link, Navigation, action, canonical_url, enabled_with_permission,
51+ LaunchpadView, LaunchpadEditFormView, LaunchpadFormView,
52+ Navigation, action, canonical_url,
53 safe_action, stepthrough, stepto, custom_widget)
54 from canonical.launchpad.webapp.authorization import check_permission
55-from canonical.launchpad.webapp.interfaces import ILaunchBag
56-from lp.registry.browser.mentoringoffer import CanBeMentoredView
57+from canonical.launchpad.webapp.menu import (
58+ ContextMenu, enabled_with_permission, Link, NavigationMenu)
59 from canonical.launchpad.browser.launchpad import AppFrontPageSearchView
60
61
62@@ -272,7 +273,32 @@
63 return self.context.getSprintSpecification(name)
64
65
66-class SpecificationContextMenu(ContextMenu):
67+class SpecificationEditLinksMixin:
68+
69+ @enabled_with_permission('launchpad.Edit')
70+ def edit(self):
71+ text = 'Change details'
72+ return Link('+edit', text, icon='edit')
73+
74+ @enabled_with_permission('launchpad.Edit')
75+ def supersede(self):
76+ text = 'Mark superseded'
77+ return Link('+supersede', text, icon='edit')
78+
79+ @enabled_with_permission('launchpad.Edit')
80+ def retarget(self):
81+ text = 'Re-target blueprint'
82+ return Link('+retarget', text, icon='edit')
83+
84+
85+class SpecificationActionMenu(NavigationMenu, SpecificationEditLinksMixin):
86+
87+ usedfor = ISpecification
88+ facet = 'specifications'
89+ links = ('edit', 'supersede', 'retarget')
90+
91+
92+class SpecificationContextMenu(ContextMenu, SpecificationEditLinksMixin):
93
94 usedfor = ISpecification
95 links = ['edit', 'people', 'status', 'priority',
96@@ -280,16 +306,10 @@
97 'milestone', 'requestfeedback', 'givefeedback', 'subscription',
98 'subscribeanother',
99 'linkbug', 'unlinkbug', 'linkbranch',
100- 'offermentoring', 'retractmentoring',
101 'adddependency', 'removedependency',
102 'dependencytree', 'linksprint', 'supersede',
103 'retarget']
104
105- @enabled_with_permission('launchpad.Edit')
106- def edit(self):
107- text = 'Change details'
108- return Link('+edit', text, icon='edit')
109-
110 def givefeedback(self):
111 text = 'Give feedback'
112 enabled = (self.user is not None and
113@@ -306,7 +326,7 @@
114 text = 'Change people'
115 return Link('+people', text, icon='edit')
116
117- @enabled_with_permission('launchpad.Edit')
118+ @enabled_with_permission('launchpad.Admin')
119 def priority(self):
120 text = 'Change priority'
121 return Link('+priority', text, icon='edit')
122@@ -335,24 +355,6 @@
123 text = 'Change status'
124 return Link('+status', text, icon='edit')
125
126- @enabled_with_permission('launchpad.AnyPerson')
127- def offermentoring(self):
128- text = 'Offer mentorship'
129- user = getUtility(ILaunchBag).user
130- enabled = self.context.canMentor(user)
131- return Link('+mentor', text, icon='add', enabled=enabled)
132-
133- def retractmentoring(self):
134- text = 'Retract mentorship'
135- user = getUtility(ILaunchBag).user
136- # We should really only allow people to retract mentoring if the
137- # spec's open and the user's already a mentor.
138- if user and not self.context.is_complete:
139- enabled = self.context.isMentor(user)
140- else:
141- enabled = False
142- return Link('+retractmentoring', text, icon='remove', enabled=enabled)
143-
144 def subscribeanother(self):
145 """Return the 'Subscribe someone else' Link."""
146 text = 'Subscribe someone else'
147@@ -372,16 +374,12 @@
148 icon = 'add'
149 return Link('+subscribe', text, icon=icon)
150
151- @enabled_with_permission('launchpad.Edit')
152- def supersede(self):
153- text = 'Mark superseded'
154- return Link('+supersede', text, icon='edit')
155-
156 @enabled_with_permission('launchpad.AnyPerson')
157 def linkbug(self):
158 text = 'Link a bug report'
159 return Link('+linkbug', text, icon='add')
160
161+ @enabled_with_permission('launchpad.AnyPerson')
162 def unlinkbug(self):
163 text = 'Unlink a bug'
164 enabled = bool(self.context.bugs)
165@@ -409,11 +407,6 @@
166 text = 'Propose for sprint'
167 return Link('+linksprint', text, icon='add')
168
169- @enabled_with_permission('launchpad.Edit')
170- def retarget(self):
171- text = 'Re-target blueprint'
172- return Link('+retarget', text, icon='edit')
173-
174 @enabled_with_permission('launchpad.AnyPerson')
175 def whiteboard(self):
176 text = 'Edit whiteboard'
177@@ -427,7 +420,8 @@
178 text = 'Link a related branch'
179 return Link('+linkbranch', text, icon='add')
180
181-class SpecificationSimpleView(LaunchpadView, CanBeMentoredView):
182+
183+class SpecificationSimpleView(LaunchpadView):
184 """Used to render portlets and listing items that need browser code."""
185
186 __used_for__ = ISpecification
187@@ -489,16 +483,18 @@
188 essential = request.form.get('essential') == 'yes'
189 if sub is not None:
190 self.context.subscribe(self.user, self.user, essential)
191- self.notices.append("You have subscribed to this spec.")
192+ self.notices.append(
193+ "You have subscribed to this blueprint.")
194 elif upd is not None:
195 self.context.subscribe(self.user, self.user, essential)
196 self.notices.append('Your subscription has been updated.')
197 elif unsub is not None:
198 self.context.unsubscribe(self.user)
199- self.notices.append("You have unsubscribed from this spec.")
200+ self.notices.append(
201+ "You have unsubscribed from this blueprint.")
202
203 if self.feedbackrequests:
204- msg = "You have %d feedback request(s) on this specification."
205+ msg = "You have %d feedback request(s) on this blueprint."
206 msg %= len(self.feedbackrequests)
207 self.notices.append(msg)
208
209@@ -529,7 +525,7 @@
210 newstate = self.context.updateLifecycleStatus(self.user)
211 if newstate is not None:
212 self.request.response.addNotification(
213- 'Specification is now considered "%s".' % newstate.title)
214+ 'blueprint is now considered "%s".' % newstate.title)
215 self.next_url = canonical_url(self.context)
216
217
218@@ -693,7 +689,7 @@
219 elif IDistribution.providedBy(target):
220 distribution = target
221 else:
222- raise AssertionError, 'Unknown target'
223+ raise AssertionError('Unknown target.')
224 self.context.retarget(product=product, distribution=distribution)
225 self._nextURL = canonical_url(self.context)
226
227@@ -747,8 +743,8 @@
228 vocabulary=SimpleVocabulary(terms),
229 required=False,
230 description=_(
231- "The specification which supersedes this one. Note "
232- "that selecting a specification here and pressing "
233+ "The blueprint which supersedes this one. Note "
234+ "that selecting a blueprint here and pressing "
235 "Continue will change the specification status "
236 "to Superseded.")),
237 render_context=self.render_context)
238
239=== modified file 'lib/lp/blueprints/browser/specificationtarget.py'
240--- lib/lp/blueprints/browser/specificationtarget.py 2009-09-22 15:02:41 +0000
241+++ lib/lp/blueprints/browser/specificationtarget.py 2009-11-14 04:37:12 +0000
242@@ -31,9 +31,10 @@
243
244 from canonical.config import config
245 from canonical.launchpad import _
246-from canonical.launchpad.webapp import LaunchpadView, Link
247+from canonical.launchpad.webapp import LaunchpadView
248 from canonical.launchpad.webapp.batching import BatchNavigator
249 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
250+from canonical.launchpad.webapp.menu import enabled_with_permission, Link
251 from canonical.launchpad.helpers import shortlist
252 from canonical.cachedproperty import cachedproperty
253 from canonical.launchpad.webapp import canonical_url
254@@ -81,6 +82,12 @@
255 text = 'Register a blueprint'
256 return Link('+addspec', text, icon='add')
257
258+ @enabled_with_permission('launchpad.View')
259+ def register_sprint(self):
260+ text = 'Register a meeting'
261+ summary = 'Register a developer sprint, summit, or gathering'
262+ return Link('/sprints/+new', text, summary=summary, icon='add')
263+
264
265 class HasSpecificationsView(LaunchpadView):
266 """Base class for several context-specific views that involve lists of
267@@ -117,9 +124,8 @@
268 is_sprint = False
269 has_drivers = False
270
271- # XXX: jsk: 2007-07-12: This method might be improved by
272+ # XXX: jsk: 2007-07-12 bug=173972: This method might be improved by
273 # replacing the conditional execution with polymorphism.
274- # See https://bugs.launchpad.net/blueprint/+bug/173972.
275 def initialize(self):
276 if IPerson.providedBy(self.context):
277 self.is_person = True
278@@ -145,7 +151,7 @@
279 self.is_sprint = True
280 self.show_target = True
281 else:
282- raise AssertionError, 'Unknown blueprint listing site'
283+ raise AssertionError('Unknown blueprint listing site.')
284
285 if IHasDrivers.providedBy(self.context):
286 self.has_drivers = True
287@@ -162,6 +168,8 @@
288 else:
289 return _('Blueprints for $name', mapping=mapping)
290
291+ page_title = 'Blueprints'
292+
293 def mdzCsv(self):
294 """Quick hack for mdz, to get csv dump of specs."""
295 import csv
296@@ -382,7 +390,9 @@
297 class RegisterABlueprintButtonView:
298 """View that renders a button to register a blueprint on its context."""
299
300- def __call__(self):
301+ @cachedproperty
302+ def target_url(self):
303+ """The +addspec URL for the specifiation target or None"""
304 # Check if the context has an +addspec view available.
305 if queryMultiAdapter(
306 (self.context, self.request), name='+addspec'):
307@@ -390,15 +400,25 @@
308 else:
309 # otherwise find an adapter to ISpecificationTarget which will.
310 target = ISpecificationTarget(self.context)
311+ if target is None:
312+ return None
313+ else:
314+ return canonical_url(
315+ target, rootsite='blueprints', view_name='+addspec')
316
317+ def __call__(self):
318+ if self.target_url is None:
319+ return ''
320 return """
321- <a href="%s/+addspec" id="addspec">
322- <img
323- alt="Register a blueprint"
324- src="/+icing/but-sml-registerablueprint.gif"
325- />
326- </a>
327- """ % canonical_url(target, rootsite='blueprints')
328+ <div id="involvement" class="portlet involvement">
329+ <ul>
330+ <li style="border: none">
331+ <a class="menu-link-register_blueprint sprite blueprints"
332+ href="%s">Register a blueprint</a>
333+ </li>
334+ </ul>
335+ </div>
336+ """ % self.target_url
337
338
339 class BlueprintsVHostBreadcrumb(Breadcrumb):
340
341=== modified file 'lib/lp/blueprints/browser/sprint.py'
342--- lib/lp/blueprints/browser/sprint.py 2009-10-30 16:38:20 +0000
343+++ lib/lp/blueprints/browser/sprint.py 2009-11-14 04:37:12 +0000
344@@ -149,6 +149,10 @@
345 # a second h1 to display. But as this view implements IMajorHeadingView
346 # it should not include an h1 below the app buttons.
347 label = None
348+
349+ @property
350+ def page_title(self):
351+ return '%s (sprint or meeting)' % self.context.title
352
353 def initialize(self):
354 self.notices = []
355@@ -340,6 +344,8 @@
356 return smartquote(
357 'Review discussion topics for "%s" sprint' % self.context.title)
358
359+ page_title = label
360+
361 def initialize(self):
362 self.status_message = None
363 self.process_form()
364@@ -470,15 +476,22 @@
365 class SprintSetNavigationMenu(RegistryCollectionActionMenuBase):
366 """Action menu for sprints index."""
367 usedfor = ISprintSet
368- links = [
369+ links = (
370 'register_team',
371 'register_project',
372+ 'register_sprint',
373 'create_account',
374 'view_all_sprints',
375- ]
376+ )
377+
378+ @enabled_with_permission('launchpad.View')
379+ def register_sprint(self):
380+ text = 'Register a meeting'
381+ summary = 'Register a developer sprint, summit, or gathering'
382+ return Link('+new', text, summary=summary, icon='add')
383
384 def view_all_sprints(self):
385- text = 'Show all sprints'
386+ text = 'Show all meetings'
387 return Link('+all', text, icon='list')
388
389
390
391=== added file 'lib/lp/blueprints/browser/tests/test_menus.py'
392--- lib/lp/blueprints/browser/tests/test_menus.py 1970-01-01 00:00:00 +0000
393+++ lib/lp/blueprints/browser/tests/test_menus.py 2009-11-14 04:37:13 +0000
394@@ -0,0 +1,40 @@
395+# Copyright 2009 Canonical Ltd. This software is licensed under the
396+# GNU Affero General Public License version 3 (see the file LICENSE).
397+
398+__metaclass__ = type
399+
400+import unittest
401+
402+from canonical.testing.layers import DatabaseFunctionalLayer
403+
404+from lp.blueprints.browser.specification import (
405+ SpecificationActionMenu, SpecificationContextMenu)
406+from lp.testing import TestCaseWithFactory
407+from lp.testing.menu import check_menu_links
408+
409+
410+class TestSpecificationMenus(TestCaseWithFactory):
411+ """Test specification menus links."""
412+ layer = DatabaseFunctionalLayer
413+
414+ def setUp(self):
415+ TestCaseWithFactory.setUp(self)
416+ self.specification = self.factory.makeSpecification()
417+
418+ def test_SpecificationContextMenu(self):
419+ menu = SpecificationContextMenu(self.specification)
420+ self.assertTrue(check_menu_links(menu))
421+
422+ def test_SpecificationActionMenu(self):
423+ menu = SpecificationActionMenu(self.specification)
424+ self.assertTrue(check_menu_links(menu))
425+
426+
427+def test_suite():
428+ suite = unittest.TestSuite()
429+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
430+ return suite
431+
432+
433+if __name__ == '__main__':
434+ unittest.TextTestRunner().run(test_suite())
435
436=== added file 'lib/lp/blueprints/browser/tests/test_specificationtarget.py'
437--- lib/lp/blueprints/browser/tests/test_specificationtarget.py 1970-01-01 00:00:00 +0000
438+++ lib/lp/blueprints/browser/tests/test_specificationtarget.py 2009-11-14 04:37:13 +0000
439@@ -0,0 +1,93 @@
440+# Copyright 2009 Canonical Ltd. This software is licensed under the
441+# GNU Affero General Public License version 3 (see the file LICENSE).
442+
443+__metaclass__ = type
444+
445+import unittest
446+
447+from canonical.launchpad.layers import BlueprintLayer
448+from canonical.testing.layers import DatabaseFunctionalLayer
449+
450+from lp.blueprints.interfaces.specificationtarget import (
451+ IHasSpecifications, ISpecificationTarget)
452+from lp.testing import login_person, TestCaseWithFactory
453+from lp.testing.views import create_view
454+
455+
456+class TestRegisterABlueprintButtonView(TestCaseWithFactory):
457+ """Test specification menus links."""
458+ layer = DatabaseFunctionalLayer
459+
460+ def verify_view(self, context, name):
461+ view = create_view(
462+ context, '+register-a-blueprint-button')
463+ self.assertEqual(
464+ 'http://blueprints.launchpad.dev/%s/+addspec' % name,
465+ view.target_url)
466+ self.assertTrue(
467+ '<div id="involvement" class="portlet involvement">' in view())
468+
469+ def test_specificationtarget(self):
470+ context = self.factory.makeProduct(name='almond')
471+ self.assertTrue(ISpecificationTarget.providedBy(context))
472+ self.verify_view(context, context.name)
473+
474+ def test_adaptable_to_specificationtarget(self):
475+ context = self.factory.makeProject(name='hazelnut')
476+ self.assertFalse(ISpecificationTarget.providedBy(context))
477+ self.verify_view(context, context.name)
478+
479+ def test_sprint(self):
480+ # Sprints are a special case. They are not ISpecificationTargets,
481+ # nor can they be adapted to a ISpecificationTarget,
482+ # but can create a spcification for a ISpecificationTarget.
483+ context = self.factory.makeSprint(title='Walnut', name='walnut')
484+ self.assertFalse(ISpecificationTarget.providedBy(context))
485+ self.verify_view(context, 'sprints/%s' % context.name)
486+
487+
488+class TestHasSpecificationsView(TestCaseWithFactory):
489+ """Test specification menus links."""
490+ layer = DatabaseFunctionalLayer
491+
492+ def setUp(self):
493+ TestCaseWithFactory.setUp(self)
494+ self.user = self.factory.makePerson(name="macadamia")
495+ login_person(self.user)
496+
497+ def verify_involvment(self, context):
498+ self.assertTrue(IHasSpecifications.providedBy(context))
499+ view = create_view(
500+ context, '+specs', layer=BlueprintLayer, principal=self.user)
501+ self.assertTrue(
502+ '<div id="involvement" class="portlet involvement">' in view())
503+
504+ def test_specificationtarget(self):
505+ context = self.factory.makeProduct(name='almond')
506+ self.verify_involvment(context)
507+
508+ def test_adaptable_to_specificationtarget(self):
509+ context = self.factory.makeProject(name='hazelnut')
510+ self.verify_involvment(context)
511+
512+ def test_sprint(self):
513+ context = self.factory.makeSprint(title='Walnut', name='walnut')
514+ self.verify_involvment(context)
515+
516+ def test_person(self):
517+ context = self.factory.makePerson(name='pistachio')
518+ self.assertTrue(IHasSpecifications.providedBy(context))
519+ view = create_view(
520+ context, '+specs', layer=BlueprintLayer, principal=self.user)
521+ self.assertFalse(
522+ '<div id="involvement" class="portlet involvement">' in view())
523+
524+
525+def test_suite():
526+ suite = unittest.TestSuite()
527+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
528+ return suite
529+
530+
531+if __name__ == '__main__':
532+ unittest.TextTestRunner().run(test_suite())
533
534=== modified file 'lib/lp/blueprints/stories/blueprints/01-creation.txt'
535--- lib/lp/blueprints/stories/blueprints/01-creation.txt 2009-09-22 15:02:41 +0000
536+++ lib/lp/blueprints/stories/blueprints/01-creation.txt 2009-11-14 04:37:12 +0000
537@@ -52,11 +52,12 @@
538
539 >>> user_browser.open('http://blueprints.launchpad.dev/ubuntu')
540
541-Users can press the graphical "Register a blueprint" button:
542+Users can use the ISpecificationTarget involvement menu to register a
543+blueprint.
544
545- >>> print find_tag_by_id(user_browser.contents, 'addspec')
546- <a href="http://blueprints.launchpad.dev/ubuntu/+addspec"
547- id="addspec"> <img alt="Register a blueprint"...
548+ >>> print extract_text(
549+ ... find_tag_by_id(user_browser.contents, 'involvement').a)
550+ Register a blueprint
551
552 Users can also follow the textual "Register a blueprint" link:
553
554@@ -73,12 +74,6 @@
555
556 >>> user_browser.open('http://blueprints.launchpad.dev/ubuntu/hoary')
557
558-Users can press the graphical "Register a blueprint" button:
559-
560- >>> print find_tag_by_id(user_browser.contents, 'addspec')
561- <a href="http://blueprints.launchpad.dev/ubuntu/hoary/+addspec"
562- id="addspec"> <img alt="Register a blueprint"...
563-
564 Users can also follow the textual "Register a blueprint" link:
565
566 >>> for tag in find_tags_by_class(
567@@ -94,11 +89,6 @@
568
569 >>> user_browser.open('http://blueprints.launchpad.dev/bzr')
570
571-Users can press the graphical "Register a blueprint" button:
572-
573- >>> print find_tag_by_id(user_browser.contents, 'addspec')
574- <a href="http://blueprints.launchpad.dev/bzr/+addspec"
575- id="addspec"> <img alt="Register a blueprint"...
576
577 Users can also follow the textual "Register a blueprint" link:
578
579@@ -125,11 +115,6 @@
580
581 >>> user_browser.open('http://blueprints.launchpad.dev/firefox/1.0')
582
583-Users can press the graphical "Register a blueprint" button:
584-
585- >>> print find_tag_by_id(user_browser.contents, 'addspec')
586- <a href="http://blueprints.launchpad.dev/firefox/1.0/+addspec"
587- id="addspec"> <img alt="Register a blueprint" ...
588
589 Users can also follow the textual "Register a blueprint" link:
590
591@@ -146,12 +131,6 @@
592
593 >>> user_browser.open('http://blueprints.launchpad.dev/mozilla')
594
595-Users can press the graphical "Register a blueprint" button:
596-
597- >>> print find_tag_by_id(user_browser.contents, 'addspec')
598- <a href="http://blueprints.launchpad.dev/mozilla/+addspec"
599- id="addspec"> <img alt="Register a blueprint" ...
600-
601 Users can follow the textual "Register a blueprint" link:
602
603 >>> for tag in find_tags_by_class(
604@@ -167,12 +146,6 @@
605
606 >>> user_browser.open('http://blueprints.launchpad.dev/sprints/futurista')
607
608-Users can press the graphical "Register a blueprint" button:
609-
610- >>> print find_tag_by_id(user_browser.contents, 'addspec')
611- <a href="http://blueprints.launchpad.dev/sprints/futurista/+addspec"
612- id="addspec"> <img alt="Register a blueprint"...
613-
614 Users can also follow the textual "Register a blueprint" link:
615
616 >>> for tag in find_tags_by_class(
617
618=== modified file 'lib/lp/blueprints/stories/blueprints/02-buglinks.txt'
619--- lib/lp/blueprints/stories/blueprints/02-buglinks.txt 2009-09-18 23:33:06 +0000
620+++ lib/lp/blueprints/stories/blueprints/02-buglinks.txt 2009-11-14 04:37:12 +0000
621@@ -13,7 +13,7 @@
622 ... 'http://launchpad.dev/firefox/+spec/svg-support')
623 >>> print extract_text(find_tag_by_id(anon_browser.contents, 'bug_links'))
624 Related bugs
625- Bug #1: Firefox does not support SVG ...
626+ Bug #1: Firefox does not support SVG
627
628
629 == Adding Links ==
630
631=== modified file 'lib/lp/blueprints/stories/sprints/05-sprint-creation.txt'
632--- lib/lp/blueprints/stories/sprints/05-sprint-creation.txt 2009-09-24 14:12:38 +0000
633+++ lib/lp/blueprints/stories/sprints/05-sprint-creation.txt 2009-11-14 04:37:12 +0000
634@@ -3,7 +3,8 @@
635 We should also be able to create a new sprint. We do this off the Sprints
636 +new page.
637
638- >>> user_browser.open('http://launchpad.dev/sprints/+new')
639+ >>> user_browser.open('http://launchpad.dev/sprints')
640+ >>> user_browser.getLink('Register a meeting').click()
641
642 >>> print user_browser.title
643 Register a meeting...
644
645=== modified file 'lib/lp/blueprints/stories/standalone/subscribing.txt'
646--- lib/lp/blueprints/stories/standalone/subscribing.txt 2009-09-22 20:21:30 +0000
647+++ lib/lp/blueprints/stories/standalone/subscribing.txt 2009-11-14 04:37:12 +0000
648@@ -49,7 +49,7 @@
649 subscribed.
650
651 >>> browser.getControl('Subscribe').click()
652- >>> 'You have subscribed to this spec' in browser.contents
653+ >>> 'You have subscribed to this blueprint' in browser.contents
654 True
655 >>> 'subscriber-essential' in browser.contents
656 True
657@@ -101,7 +101,7 @@
658 >>> unsubit.value
659 'Unsubscribe'
660 >>> unsubit.click()
661- >>> 'You have unsubscribed from this spec.' in browser.contents
662+ >>> 'You have unsubscribed from this blueprint.' in browser.contents
663 True
664 >>> 'subscriber-inessential' in browser.contents
665 False
666
667=== modified file 'lib/lp/blueprints/templates/hasspecifications-specs.pt'
668--- lib/lp/blueprints/templates/hasspecifications-specs.pt 2009-09-22 15:02:41 +0000
669+++ lib/lp/blueprints/templates/hasspecifications-specs.pt 2009-11-14 04:37:12 +0000
670@@ -10,6 +10,8 @@
671 <body>
672
673 <tal:side metal:fill-slot="side">
674+ <tal:register-blueprint
675+ content="structure context/@@+register-a-blueprint-button|nothing" />
676 <tal:menu replace="structure context/@@+global-actions" />
677 <div tal:replace="structure context/@@+portlet-latestspecs" />
678 </tal:side>
679@@ -31,9 +33,6 @@
680 </tal:block>
681 </tal:block>
682
683- <div style="float: right;"
684- tal:content="structure context/@@+register-a-blueprint-button|nothing" />
685-
686 <tal:no_specs condition="not: has_any_specs">
687 <p>
688 Launchpad lets projects track the features they intend to implement over
689@@ -107,14 +106,11 @@
690 In this case, the drivers are:
691 </p>
692
693- <blockquote>
694- <ul>
695- <li tal:repeat="driver context/drivers" class="person">
696- <a tal:attributes="href driver/fmt:url"
697- tal:content="driver/displayname">Foo Bar</a>
698- </li>
699- </ul>
700- </blockquote>
701+ <ul class="subordinate">
702+ <li tal:repeat="driver context/drivers">
703+ <a tal:replace="structure driver/fmt:link">Foo Bar</a>
704+ </li>
705+ </ul>
706 </tal:not_driver>
707 <tal:is_driver condition="context/required:launchpad.Driver">
708 Since you are a driver of
709@@ -130,14 +126,13 @@
710 release management with Launchpad.
711 </p>
712
713- <blockquote>
714- <ul class="menu">
715- <li class="info">
716- <a href="https://help.launchpad.net/BlueprintDocumentation"
717- >Read more about tracking blueprints</a>
718- </li>
719- </ul>
720- </blockquote>
721+ <ul class="horizontal">
722+ <li>
723+ <a class="info sprite"
724+ href="https://help.launchpad.net/BlueprintDocumentation">Read
725+ more about tracking blueprints</a>
726+ </li>
727+ </ul>
728
729 </tal:no_specs>
730 <tal:has_specs condition="has_any_specs">
731
732=== modified file 'lib/lp/blueprints/templates/specification-index.pt'
733--- lib/lp/blueprints/templates/specification-index.pt 2009-09-23 08:20:02 +0000
734+++ lib/lp/blueprints/templates/specification-index.pt 2009-11-14 04:37:12 +0000
735@@ -347,19 +347,7 @@
736 </div>
737
738 <tal:side metal:fill-slot="side">
739- <div class="portlet">
740- <ul>
741- <li tal:condition="context/menu:context/edit/enabled">
742- <a tal:replace="structure context/menu:context/edit/fmt:link" />
743- </li>
744- <li tal:condition="context/menu:context/supersede/enabled">
745- <a tal:replace="structure context/menu:context/supersede/fmt:link" />
746- </li>
747- <li tal:condition="context/menu:context/retarget/enabled">
748- <a tal:replace="structure context/menu:context/retarget/fmt:link" />
749- </li>
750- </ul>
751- </div>
752+ <tal:menu replace="structure context/@@+global-actions" />
753
754 <div tal:replace="structure context/@@+portlet-subscribers" />
755 </tal:side>
756
757=== modified file 'lib/lp/blueprints/templates/specificationtarget-documentation.pt'
758--- lib/lp/blueprints/templates/specificationtarget-documentation.pt 2009-09-17 21:20:14 +0000
759+++ lib/lp/blueprints/templates/specificationtarget-documentation.pt 2009-11-14 04:37:13 +0000
760@@ -10,10 +10,9 @@
761 <body>
762 <metal:main fill-slot="main" tal:define="specs view/documentation">
763 <p>
764- To classify a blueprint as documentation, check the
765- &#8220;Is Informational&#8221; box on its &#8220;Edit Details&#8221;
766- page. When the blueprint is marked "Approved" then it will show
767- up in this listing.
768+ To classify a blueprint as documentation, set the Implementation status
769+ to &#8220;Informational&#8221; When the blueprint's Definition status is
770+ marked &#8220;Approved&#8221;, it will appear in this listing.
771 </p>
772
773 <table id="documentation-listing-table">
774
775=== modified file 'lib/lp/blueprints/templates/sprint-portlet-attendees.pt'
776--- lib/lp/blueprints/templates/sprint-portlet-attendees.pt 2009-09-23 13:49:22 +0000
777+++ lib/lp/blueprints/templates/sprint-portlet-attendees.pt 2009-11-14 04:37:13 +0000
778@@ -11,7 +11,7 @@
779 <div class="portletBody portletContent">
780
781 <ul>
782- <li class="person" tal:repeat="attendance context/attendances">
783+ <li tal:repeat="attendance context/attendances">
784 <a tal:replace="structure attendance/attendee/fmt:link">Foo Bar</a><br />
785 <div class="discreet">
786 <span tal:replace="python:view.formatDate(attendance.time_starts)">
787
788=== modified file 'lib/lp/blueprints/templates/sprint-register.pt'
789--- lib/lp/blueprints/templates/sprint-register.pt 2009-09-18 17:44:08 +0000
790+++ lib/lp/blueprints/templates/sprint-register.pt 2009-11-14 04:37:12 +0000
791@@ -8,6 +8,10 @@
792 >
793
794 <body>
795+ <metal:block fill-slot="head_epilogue">
796+ <metal:yui-dependencies
797+ use-macro="context/@@launchpad_widget_macros/yui2calendar-dependencies" />
798+ </metal:block>
799
800 <div metal:fill-slot="main">
801 <div metal:use-macro="context/@@launchpad_form/form">
802
803=== modified file 'lib/lp/bugs/browser/buglinktarget.py'
804--- lib/lp/bugs/browser/buglinktarget.py 2009-08-13 17:04:41 +0000
805+++ lib/lp/bugs/browser/buglinktarget.py 2009-11-14 04:37:12 +0000
806@@ -30,19 +30,15 @@
807 class BugLinkView(LaunchpadFormView):
808 """This view is used to link bugs to any IBugLinkTarget."""
809
810- label = _('Link to bug report')
811-
812+ label = _('Link a bug report')
813 schema = IBugLinkForm
814
815 focused_element_id = 'bug'
816
817 @property
818- def page_title(self):
819- return 'Link question #%s to a bug report' % self.context.id
820-
821- @property
822- def label(self):
823- return 'Link question to a bug report'
824+ def cancel_url(self):
825+ """See `LaunchpadFormview`."""
826+ return canonical_url(self.context)
827
828 @action(_('Link'))
829 def linkBug(self, action, data):
830@@ -98,17 +94,13 @@
831 """This view is used to remove bug links from any IBugLinkTarget."""
832
833 label = _('Remove links to bug reports')
834-
835 schema = IUnlinkBugsForm
836 custom_widget('bugs', LabeledMultiCheckBoxWidget)
837
838 @property
839- def page_title(self):
840- return 'Remove bug links from question #%s' % self.context.id
841-
842- @property
843- def label(self):
844- return 'Remove links to bug reports'
845+ def cancel_url(self):
846+ """See `LaunchpadFormview`."""
847+ return canonical_url(self.context)
848
849 @action(_('Remove'))
850 def unlinkBugs(self, action, data):
851@@ -134,4 +126,3 @@
852 """
853 return [bug for bug in self.context.bugs
854 if check_permission('launchpad.View', bug)]
855-
856
857=== modified file 'lib/lp/bugs/browser/tests/buglinktarget-views.txt'
858--- lib/lp/bugs/browser/tests/buglinktarget-views.txt 2009-06-12 16:36:02 +0000
859+++ lib/lp/bugs/browser/tests/buglinktarget-views.txt 2009-11-14 04:37:13 +0000
860@@ -24,8 +24,16 @@
861
862 == Link Bug View ==
863
864-The +linkbug view is used to link bugs to IBugLinkTarget. It has a
865-simple widget to enter the bug number or nickname of the bug to link
866+The +linkbug view is used to link bugs to IBugLinkTarget.
867+
868+ >>> view = create_view(cve, name='+linkbug')
869+ >>> print view.label
870+ Link a bug report
871+
872+ >>> print view.cancel_url
873+ http://launchpad.dev/bugs/cve/2005-2730
874+
875+It has a simple widget to enter the bug number or nickname of the bug to link
876 to. After it links the bug, it sends a ObjectModifiedEvent.
877
878 >>> request = LaunchpadTestRequest(
879@@ -70,8 +78,16 @@
880
881
882 The +unlinkbug view is used to unlink a selection of bugs from an
883-IBugLinkTarget. After removing the bugs, it sends a SQLObjectModified
884-event.
885+IBugLinkTarget.
886+
887+ >>> view = create_view(cve, name='+unlinkbug')
888+ >>> print view.label
889+ Remove links to bug reports
890+
891+ >>> print view.cancel_url
892+ http://launchpad.dev/bugs/cve/2005-2730
893+
894+After removing the bugs, it sends a SQLObjectModified event.
895
896 >>> request = LaunchpadTestRequest(
897 ... method="POST",
898
899=== modified file 'lib/lp/coop/answersbugs/stories/question-buglink.txt'
900--- lib/lp/coop/answersbugs/stories/question-buglink.txt 2009-09-18 15:24:30 +0000
901+++ lib/lp/coop/answersbugs/stories/question-buglink.txt 2009-11-14 04:37:12 +0000
902@@ -70,7 +70,7 @@
903
904 >>> user_browser.getLink('Remove bug link').click()
905 >>> print user_browser.title
906- Remove bug links from question #2...
907+ Remove links to bug reports : Question #...
908
909 The list of linked bugs is displayed. The user selects the link to
910 remove and clicks the 'Remove' button.
911
912=== modified file 'lib/lp/registry/browser/distribution.py'
913--- lib/lp/registry/browser/distribution.py 2009-11-10 11:31:06 +0000
914+++ lib/lp/registry/browser/distribution.py 2009-11-14 04:37:13 +0000
915@@ -452,7 +452,7 @@
916 HasSpecificationsMenuMixin):
917 usedfor = IDistribution
918 facet = 'specifications'
919- links = ['listall', 'doc', 'assignments', 'new']
920+ links = ['listall', 'doc', 'assignments', 'new', 'register_sprint']
921
922
923 class DistributionPackageSearchView(PackageSearchViewBase):
924
925=== modified file 'lib/lp/registry/browser/distroseries.py'
926--- lib/lp/registry/browser/distroseries.py 2009-11-07 01:45:32 +0000
927+++ lib/lp/registry/browser/distroseries.py 2009-11-14 04:37:13 +0000
928@@ -227,7 +227,9 @@
929
930 usedfor = IDistroSeries
931 facet = 'specifications'
932- links = ['listall', 'listdeclined', 'assignments', 'setgoals', 'new']
933+ links = [
934+ 'listall', 'listdeclined', 'assignments', 'setgoals',
935+ 'new', 'register_sprint']
936
937
938 class DistroSeriesPackageSearchView(PackageSearchViewBase):
939
940=== modified file 'lib/lp/registry/browser/person.py'
941--- lib/lp/registry/browser/person.py 2009-10-30 14:55:25 +0000
942+++ lib/lp/registry/browser/person.py 2009-11-14 04:37:13 +0000
943@@ -1808,6 +1808,7 @@
944 class PersonSpecFeedbackView(HasSpecificationsView):
945
946 label = 'Feature feedback requests'
947+ page_title = label
948
949 @cachedproperty
950 def feedback_specs(self):
951
952=== modified file 'lib/lp/registry/browser/product.py'
953--- lib/lp/registry/browser/product.py 2009-11-07 00:49:26 +0000
954+++ lib/lp/registry/browser/product.py 2009-11-14 04:37:12 +0000
955@@ -468,7 +468,7 @@
956 HasSpecificationsMenuMixin):
957 usedfor = IProduct
958 facet = 'specifications'
959- links = ['listall', 'doc', 'assignments', 'new']
960+ links = ['listall', 'doc', 'assignments', 'new', 'register_sprint']
961
962
963 def _sort_distros(a, b):
964
965=== modified file 'lib/lp/registry/browser/productseries.py'
966--- lib/lp/registry/browser/productseries.py 2009-11-11 00:19:14 +0000
967+++ lib/lp/registry/browser/productseries.py 2009-11-14 04:37:13 +0000
968@@ -256,7 +256,9 @@
969
970 usedfor = IProductSeries
971 facet = 'specifications'
972- links = ['listall', 'assignments', 'setgoals', 'listdeclined', 'new']
973+ links = [
974+ 'listall', 'assignments', 'setgoals', 'listdeclined',
975+ 'new', 'register_sprint']
976
977
978 class ProductSeriesOverviewNavigationMenu(NavigationMenu):
979
980=== modified file 'lib/lp/registry/browser/project.py'
981--- lib/lp/registry/browser/project.py 2009-11-10 05:05:49 +0000
982+++ lib/lp/registry/browser/project.py 2009-11-14 04:37:13 +0000
983@@ -273,7 +273,7 @@
984 HasSpecificationsMenuMixin):
985 usedfor = IProject
986 facet = 'specifications'
987- links = ['listall', 'doc', 'assignments', 'new']
988+ links = ['listall', 'doc', 'assignments', 'new', 'register_sprint']
989
990
991 class ProjectAnswersMenu(QuestionCollectionAnswersMenu):
992
993=== modified file 'lib/lp/registry/browser/tests/product-menus.txt'
994--- lib/lp/registry/browser/tests/product-menus.txt 2009-10-08 18:15:31 +0000
995+++ lib/lp/registry/browser/tests/product-menus.txt 2009-11-14 04:37:12 +0000
996@@ -9,32 +9,12 @@
997 The check_menu_links() function is defined here to make it simple to
998 test all the product menu classes below.
999
1000- >>> from zope.component import getMultiAdapter
1001- >>> from canonical.lazr.testing.menus import make_fake_request
1002- >>> from canonical.launchpad.webapp.publisher import canonical_url
1003- >>> from lp.registry.interfaces.product import IProductSet
1004-
1005- >>> def check_menu_links(menu):
1006- ... context = menu.context
1007- ... is_sane_menu = True
1008- ... for link in menu.iterlinks():
1009- ... if '?' in link.target:
1010- ... view_name, _args = link.target.split('?')
1011- ... else:
1012- ... view_name = link.target
1013- ... url = canonical_url(context, view_name=view_name)
1014- ... request = make_fake_request(url)
1015- ... try:
1016- ... view = getMultiAdapter((context, request), name=view_name)
1017- ... except:
1018- ... is_sane_menu = False
1019- ... print 'Bad link %s: %s' % (link.name, url)
1020- ... print is_sane_menu
1021-
1022+ >>> from lp.testing.menu import check_menu_links
1023 >>> from lp.registry.browser.product import (
1024 ... ProductBugsMenu, ProductNavigationMenu,
1025 ... ProductOverviewMenu, ProductSpecificationsMenu)
1026- >>> product = getUtility(IProductSet)['firefox']
1027+
1028+ >>> product = factory.makeProduct()
1029
1030 >>> check_menu_links(ProductOverviewMenu(product))
1031 True
1032
1033=== modified file 'lib/lp/testing/factory.py'
1034--- lib/lp/testing/factory.py 2009-11-13 14:58:08 +0000
1035+++ lib/lp/testing/factory.py 2009-11-14 04:37:12 +0000
1036@@ -619,12 +619,13 @@
1037 description=description,
1038 owner=owner)
1039
1040- def makeSprint(self, title=None):
1041+ def makeSprint(self, title=None, name=None):
1042 """Make a sprint."""
1043 if title is None:
1044 title = self.getUniqueString('title')
1045 owner = self.makePerson()
1046- name = self.getUniqueString('name')
1047+ if name is None:
1048+ name = self.getUniqueString('name')
1049 time_starts = datetime(2009, 1, 1, tzinfo=pytz.UTC)
1050 time_ends = datetime(2009, 1, 2, tzinfo=pytz.UTC)
1051 time_zone = 'UTC'
1052
1053=== modified file 'lib/lp/testing/menu.py'
1054--- lib/lp/testing/menu.py 2009-11-13 13:06:50 +0000
1055+++ lib/lp/testing/menu.py 2009-11-14 04:37:13 +0000
1056@@ -14,6 +14,9 @@
1057 def check_menu_links(menu):
1058 context = menu.context
1059 for link in menu.iterlinks():
1060+ if link.target.startswith('/'):
1061+ # The context is not the context of this target.
1062+ continue
1063 if '?' in link.target:
1064 view_name, _args = link.target.split('?')
1065 else: