Merge lp:~stevenk/launchpad/sensible-superseded-by into lp:launchpad

Proposed by Steve Kowalik on 2012-10-30
Status: Merged
Approved by: Steve Kowalik on 2012-10-30
Approved revision: no longer in the source branch.
Merged at revision: 16215
Proposed branch: lp:~stevenk/launchpad/sensible-superseded-by
Merge into: lp:launchpad
Diff against target: 289 lines (+67/-79)
5 files modified
lib/lp/blueprints/browser/specification.py (+22/-32)
lib/lp/blueprints/interfaces/specification.py (+5/-3)
lib/lp/blueprints/model/specification.py (+15/-13)
lib/lp/blueprints/stories/blueprints/xx-superseding-within-projects.txt (+3/-3)
lib/lp/blueprints/stories/blueprints/xx-superseding.txt (+22/-28)
To merge this branch: bzr merge lp:~stevenk/launchpad/sensible-superseded-by
Reviewer Review Type Date Requested Status
Ian Booth (community) 2012-10-30 Approve on 2012-10-30
Review via email: mp+132009@code.launchpad.net

Commit Message

Rip out the Blueprint superseded_by dropdown and burn it, and replace it with a text box.

Description of the Change

Rip out the Blueprint superseded_by dropdown and burn it. Replace it with a text box that allows the user to type the name of the blueprint. Sprinkle in some infrastructure into ISpecificationSet to help.

I have performed some clean up to squeak into LoC net negative, as well as reformatting a doctest to silence lint.

To post a comment you must log in.
Ian Booth (wallyworld) wrote :

Looks good, with the tweaks suggested on irc - lookup blueprint once in validation method, and result set .one()

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/specification.py'
2--- lib/lp/blueprints/browser/specification.py 2012-10-12 17:16:08 +0000
3+++ lib/lp/blueprints/browser/specification.py 2012-10-30 05:10:25 +0000
4@@ -61,7 +61,6 @@
5 TextAreaWidget,
6 TextWidget,
7 )
8-from zope.app.form.browser.itemswidgets import DropdownWidget
9 from zope.component import getUtility
10 from zope.error.interfaces import IErrorReportingUtility
11 from zope.event import notify
12@@ -75,10 +74,7 @@
13 from zope.schema import (
14 Bool,
15 Choice,
16- )
17-from zope.schema.vocabulary import (
18- SimpleTerm,
19- SimpleVocabulary,
20+ TextLine,
21 )
22
23 from lp import _
24@@ -1039,53 +1035,47 @@
25 return canonical_url(self.context)
26
27
28-class SupersededByWidget(DropdownWidget):
29- """Custom select widget for specification superseding.
30-
31- This is just a standard DropdownWidget with the (no value) text
32- rendered as something meaningful to the user, as per Bug #4116.
33-
34- TODO: This should be replaced with something more scalable as there
35- is no upper limit to the number of specifications.
36- -- StuartBishop 20060704
37- """
38- _messageNoValue = _("(Not Superseded)")
39-
40-
41 class SpecificationSupersedingView(LaunchpadFormView):
42 schema = ISpecification
43 field_names = ['superseded_by']
44 label = _('Mark blueprint superseded')
45- custom_widget('superseded_by', SupersededByWidget)
46
47 @property
48 def initial_values(self):
49- return {
50- 'superseded_by': self.context.superseded_by,
51- }
52+ return {'superseded_by': self.context.superseded_by}
53
54 def setUpFields(self):
55 """Override the setup to define own fields."""
56- if self.context.target is None:
57- raise AssertionError("No target found for this spec.")
58- specs = sorted(self.context.target.specifications(self.user),
59- key=attrgetter('name'))
60- terms = [SimpleTerm(spec, spec.name, spec.title)
61- for spec in specs if spec != self.context]
62-
63 self.form_fields = form.Fields(
64- Choice(
65+ TextLine(
66 __name__='superseded_by',
67 title=_("Superseded by"),
68- vocabulary=SimpleVocabulary(terms),
69 required=False,
70 description=_(
71 "The blueprint which supersedes this one. Note "
72- "that selecting a blueprint here and pressing "
73+ "that entering a blueprint here and pressing "
74 "Continue will change the blueprint status "
75 "to Superseded.")),
76 render_context=self.render_context)
77
78+ def _fetchSpecification(self, name):
79+ pillar = self.context.target
80+ if '/' in name:
81+ pillar, name = name.split('/')
82+ return getUtility(ISpecificationSet).getByName(pillar, name)
83+
84+ def validate(self, data):
85+ """See `LaunchpadFormView`.`"""
86+ super(SpecificationSupersedingView, self).validate(data)
87+ if data['superseded_by']:
88+ spec = self._fetchSpecification(data['superseded_by'])
89+ if spec:
90+ data['superseded_by'] = spec
91+ else:
92+ self.setFieldError(
93+ 'superseded_by',
94+ "No blueprint named '%s'." % data['superseded_by'])
95+
96 @action(_('Continue'), name='supersede')
97 def supersede_action(self, action, data):
98 # Store some shorter names to avoid line-wrapping.
99
100=== modified file 'lib/lp/blueprints/interfaces/specification.py'
101--- lib/lp/blueprints/interfaces/specification.py 2012-09-27 14:30:40 +0000
102+++ lib/lp/blueprints/interfaces/specification.py 2012-10-30 05:10:25 +0000
103@@ -1,8 +1,6 @@
104-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
105+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108-# pylint: disable-msg=E0211,E0213
109-
110 """Specification interfaces."""
111
112 __metaclass__ = type
113@@ -693,6 +691,10 @@
114 def getByURL(url):
115 """Return the specification with the given url."""
116
117+ def getByName(pillar, name):
118+ """Return the specification with the given name for the given pillar.
119+ """
120+
121 def new(name, title, specurl, summary, definition_status,
122 owner, approver=None, product=None, distribution=None, assignee=None,
123 drafter=None, whiteboard=None,
124
125=== modified file 'lib/lp/blueprints/model/specification.py'
126--- lib/lp/blueprints/model/specification.py 2012-10-22 20:04:30 +0000
127+++ lib/lp/blueprints/model/specification.py 2012-10-30 05:10:25 +0000
128@@ -1,8 +1,6 @@
129-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
130+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
131 # GNU Affero General Public License version 3 (see the file LICENSE).
132
133-# pylint: disable-msg=E0611,W0212
134-
135 __metaclass__ = type
136 __all__ = [
137 'get_specification_filters',
138@@ -108,6 +106,7 @@
139 sqlvalues,
140 )
141 from lp.services.database.stormexpr import fti_search
142+from lp.services.database.lpstorm import IStore
143 from lp.services.mail.helpers import get_contact_email_addresses
144 from lp.services.propertycache import (
145 cachedproperty,
146@@ -1001,10 +1000,7 @@
147 index += 1
148 decorator(person, column)
149
150- results = Store.of(self).find(
151- Specification,
152- *clauses
153- )
154+ results = Store.of(self).find(Specification, *clauses)
155 return DecoratedResultSet(results, pre_iter_hook=cache_people)
156
157 @property
158@@ -1021,8 +1017,8 @@
159
160 def specificationCount(self, user):
161 """See IHasSpecifications."""
162- return self.specifications(user,
163- filter=[SpecificationFilter.ALL]).count()
164+ return self.specifications(
165+ user, filter=[SpecificationFilter.ALL]).count()
166
167
168 class SpecificationSet(HasSpecificationsMixin):
169@@ -1160,10 +1156,16 @@
170
171 def getByURL(self, url):
172 """See ISpecificationSet."""
173- specification = Specification.selectOneBy(specurl=url)
174- if specification is None:
175- return None
176- return specification
177+ return Specification.selectOneBy(specurl=url)
178+
179+ def getByName(self, pillar, name):
180+ """See ISpecificationSet."""
181+ clauses = [Specification.name == name]
182+ if IDistribution.providedBy(pillar):
183+ clauses.append(Specification.distributionID == pillar.id)
184+ elif IProduct.providedBy(pillar):
185+ clauses.append(Specification.productID == pillar.id)
186+ return IStore(Specification).find(Specification, *clauses).one()
187
188 @property
189 def coming_sprints(self):
190
191=== modified file 'lib/lp/blueprints/stories/blueprints/xx-superseding-within-projects.txt'
192--- lib/lp/blueprints/stories/blueprints/xx-superseding-within-projects.txt 2010-08-26 02:30:06 +0000
193+++ lib/lp/blueprints/stories/blueprints/xx-superseding-within-projects.txt 2012-10-30 05:10:25 +0000
194@@ -1,5 +1,5 @@
195-
196-== Superseeding blueprints ==
197+Superseeding blueprints
198+=======================
199
200 Bueprints can only be superseded by other blueprints in the same project.
201 The user interface code for the superseding form passes blueprints by their
202@@ -37,7 +37,7 @@
203
204 >>> browser.open('http://blueprints.launchpad.dev/redfish/' +
205 ... '+spec/another-unique-blueprint/+supersede')
206- >>> browser.getControl('Superseded by').displayValue = ['A unique Blueprint']
207+ >>> browser.getControl('Superseded by').value = 'a-unique-blueprint'
208 >>> browser.getControl('Continue').click()
209
210 >>> 'This blueprint has been superseded' in browser.contents
211
212=== modified file 'lib/lp/blueprints/stories/blueprints/xx-superseding.txt'
213--- lib/lp/blueprints/stories/blueprints/xx-superseding.txt 2010-08-26 02:30:06 +0000
214+++ lib/lp/blueprints/stories/blueprints/xx-superseding.txt 2012-10-30 05:10:25 +0000
215@@ -8,24 +8,19 @@
216 page, we will also test that the spec is currently in the New status (i.e.
217 not already superseded).
218
219->>> browser.addHeader('Authorization', 'Basic carlos@canonical.com:test')
220->>> browser.open(
221-... 'http://blueprints.launchpad.dev/firefox/+spec/'
222-... + 'extension-manager-upgrades')
223->>> 'New' in browser.contents
224-True
225+ >>> browser.addHeader('Authorization', 'Basic carlos@canonical.com:test')
226+ >>> browser.open(
227+ ... 'http://blueprints.launchpad.dev/firefox/+spec/'
228+ ... + 'extension-manager-upgrades')
229+ >>> 'New' in browser.contents
230+ True
231
232 Make sure Bug 4116 stays fixed
233
234->>> browser.open(
235-... 'http://blueprints.launchpad.dev/firefox/+spec/'
236-... + 'extension-manager-upgrades/+supersede'
237-... )
238->>> superseded_control = browser.getControl('Superseded by')
239->>> superseded_control.options[0]
240-''
241->>> superseded_control.displayOptions[0]
242-'(Not Superseded)'
243+ >>> browser.open(
244+ ... 'http://blueprints.launchpad.dev/firefox/+spec/'
245+ ... + 'extension-manager-upgrades/+supersede'
246+ ... )
247
248 The page contains a link back to the blueprint, in case we change our
249 mind.
250@@ -38,27 +33,26 @@
251
252 Next, we will POST to that form, setting the spec which supersedes this one:
253
254->>> browser.getControl('Superseded by').value = ['svg-support']
255->>> browser.getControl('Continue').click()
256+ >>> browser.getControl('Superseded by').value = 'svg-support'
257+ >>> browser.getControl('Continue').click()
258
259 Now, on the spec page we should see an alert that the spec has been
260 superseded. The spec status should also have changed to superseded.
261
262->>> 'This blueprint has been superseded.' in browser.contents
263-True
264->>> 'Superseded' in browser.contents
265-True
266+ >>> 'This blueprint has been superseded.' in browser.contents
267+ True
268+ >>> 'Superseded' in browser.contents
269+ True
270
271 And finally, we want to clear the superseding spec data and reset the
272-status to New. If we POST back to the form, with no spec selected,
273+status to New. If we POST back to the form, with Superseded by empty,
274 then it should automatically do this:
275
276->>> browser.getLink('Mark superseded').click()
277->>> browser.getControl('Superseded by').displayValue = ['(Not Superseded)']
278->>> browser.getControl('Continue').click()
279+ >>> browser.getLink('Mark superseded').click()
280+ >>> browser.getControl('Superseded by').value = ''
281+ >>> browser.getControl('Continue').click()
282
283 Let's confirm the status change:
284
285->>> 'New' in browser.contents
286-True
287-
288+ >>> 'New' in browser.contents
289+ True