Merge lp:~nigelbabu/launchpad/specification-validation-59301 into lp:launchpad

Proposed by Nigel Babu
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 13852
Proposed branch: lp:~nigelbabu/launchpad/specification-validation-59301
Merge into: lp:launchpad
Diff against target: 335 lines (+137/-91)
4 files modified
lib/lp/blueprints/interfaces/specification.py (+8/-3)
lib/lp/blueprints/model/tests/test_specification.py (+39/-0)
lib/lp/blueprints/stories/blueprints/xx-creation.txt (+4/-3)
lib/lp/blueprints/stories/blueprints/xx-editing.txt (+86/-85)
To merge this branch: bzr merge lp:~nigelbabu/launchpad/specification-validation-59301
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+73631@code.launchpad.net

Commit message

[r=allenap][bug=59301] When attempting to use a specification URL that has already been used in another specification, link to that specification in the error message.

Description of the change

= Description =
The validation error for specurl is frustrating because it doesn't tell which other blueprint has the same specurl. This merge will mention the blueprint name and link to the blueprint.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/blueprints/interfaces/specification.py
  lib/lp/blueprints/model/tests/test_specification.py

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for doing this, it's a nice improvement.

There are some minor issues with the branch as it stands, but I doubt
they'll hold you up for long.

[1]

- errormessage = _("%s is already registered by another blueprint.")
+ errormessage = _("%s is already registered by <a href=\"%s\">%s</a>.")

I suggest using single quotes here to make it a little more readable:

    errormessage = _('%s is already registered by <a href="%s">%s</a>.')

[2]

         specification = getUtility(ISpecificationSet).getByURL(specurl)
+ specification_url = canonical_url(specification)
         if specification is not None:
- raise LaunchpadValidationError(self.errormessage % specurl)
+ raise LaunchpadValidationError(structured(self.errormessage %
+ (specurl, specification_url, specification.title)))

canonical_url(None) blows up, so move the initialization of
specification_url to within the following conditional.

There should probably be a test for this too, something like:

    def test_specurl_validation_okay(self):
        spec = self.factory.makeSpecification()
        field = ISpecification['specurl'].bind(spec)
        field.validate(u'http://example.com/nigelb')

[3]

+ raise LaunchpadValidationError(structured(self.errormessage %
+ (specurl, specification_url, specification.title)))

This is unsafe; structured() should be used slightly differently:

            raise LaunchpadValidationError(
                structured(self.errormessage, specurl, specification_url,
                           specification.title))

In the test (or in a new test) try setting the spec title to something
that must be escaped - e.g. <script>...</script> - so that escaping is
demonstrated.

[4]

+ self.assertEqual(str(e),
+ '%s is already registered by <a href="%s">%s</a>.'
+ % (u'http://ubuntu.com', url, existing.title))

The order of arguments to assertEqual() is (expected, observed). When
the test fails it refers to them as "reference" and "actual"
respectively. Just switch them around.

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good :)

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/interfaces/specification.py'
2--- lib/lp/blueprints/interfaces/specification.py 2011-06-07 06:49:18 +0000
3+++ lib/lp/blueprints/interfaces/specification.py 2011-09-02 04:07:32 +0000
4@@ -46,6 +46,8 @@
5 )
6
7 from canonical.launchpad import _
8+from canonical.launchpad.webapp import canonical_url
9+from canonical.launchpad.webapp.menu import structured
10 from lp.app.validators import LaunchpadValidationError
11 from lp.app.validators.url import valid_webref
12 from lp.blueprints.enums import (
13@@ -122,7 +124,7 @@
14
15 class SpecURLField(TextLine):
16
17- errormessage = _("%s is already registered by another blueprint.")
18+ errormessage = _('%s is already registered by <a href=\"%s\">%s</a>.')
19
20 def _validate(self, specurl):
21 TextLine._validate(self, specurl)
22@@ -133,7 +135,10 @@
23
24 specification = getUtility(ISpecificationSet).getByURL(specurl)
25 if specification is not None:
26- raise LaunchpadValidationError(self.errormessage % specurl)
27+ specification_url = canonical_url(specification)
28+ raise LaunchpadValidationError(
29+ structured(self.errormessage, specurl, specification_url,
30+ specification.title))
31
32
33 class ISpecificationPublic(IHasOwner, IHasLinkedBranches):
34@@ -373,7 +378,7 @@
35 CollectionField(
36 title=_("Branches associated with this spec, usually "
37 "branches on which this spec is being implemented."),
38- value_type=Reference(schema=Interface), # ISpecificationBranch
39+ value_type=Reference(schema=Interface), # ISpecificationBranch
40 readonly=True),
41 as_of="devel")
42
43
44=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
45--- lib/lp/blueprints/model/tests/test_specification.py 2011-07-21 19:26:51 +0000
46+++ lib/lp/blueprints/model/tests/test_specification.py 2011-09-02 04:07:32 +0000
47@@ -7,7 +7,10 @@
48
49 from testtools.matchers import Equals
50
51+from canonical.launchpad.webapp import canonical_url
52 from canonical.testing.layers import DatabaseFunctionalLayer
53+from lp.app.validators import LaunchpadValidationError
54+from lp.blueprints.interfaces.specification import ISpecification
55 from lp.testing import TestCaseWithFactory
56
57
58@@ -93,3 +96,39 @@
59 dave.displayname]
60 people = [sub.person.displayname for sub in spec.subscriptions]
61 self.assertEqual(sorted_subscriptions, people)
62+
63+
64+class TestSpecificationValidation(TestCaseWithFactory):
65+
66+ layer = DatabaseFunctionalLayer
67+
68+ def test_specurl_validation_duplicate(self):
69+ existing = self.factory.makeSpecification(
70+ specurl=u'http://ubuntu.com')
71+ spec = self.factory.makeSpecification()
72+ url = canonical_url(existing)
73+ field = ISpecification['specurl'].bind(spec)
74+ e = self.assertRaises(LaunchpadValidationError, field.validate,
75+ u'http://ubuntu.com')
76+ self.assertEqual(
77+ '%s is already registered by <a href="%s">%s</a>.'
78+ % (u'http://ubuntu.com', url, existing.title), str(e))
79+
80+ def test_specurl_validation_valid(self):
81+ spec = self.factory.makeSpecification()
82+ field = ISpecification['specurl'].bind(spec)
83+ field.validate(u'http://example.com/nigelb')
84+
85+ def test_specurl_validation_escape(self):
86+ existing = self.factory.makeSpecification(
87+ specurl=u'http://ubuntu.com/foo',
88+ title='<script>alert("foo");</script>')
89+ cleaned_title = '&lt;script&gt;alert("foo");&lt;/script&gt;'
90+ spec = self.factory.makeSpecification()
91+ url = canonical_url(existing)
92+ field = ISpecification['specurl'].bind(spec)
93+ e = self.assertRaises(LaunchpadValidationError, field.validate,
94+ u'http://ubuntu.com/foo')
95+ self.assertEqual(
96+ '%s is already registered by <a href="%s">%s</a>.'
97+ % (u'http://ubuntu.com/foo', url, cleaned_title), str(e))
98
99=== modified file 'lib/lp/blueprints/stories/blueprints/xx-creation.txt'
100--- lib/lp/blueprints/stories/blueprints/xx-creation.txt 2011-05-29 21:23:37 +0000
101+++ lib/lp/blueprints/stories/blueprints/xx-creation.txt 2011-09-02 04:07:32 +0000
102@@ -633,8 +633,8 @@
103 ... print message.renderContents()
104 There is 1 error...already in use by another blueprint...
105
106-Attempting to register a duplicate blueprint from a non-target context produces
107-the same error:
108+Attempting to register a duplicate blueprint from a non-target context
109+produces the same error:
110
111 >>> url = 'http://blueprints.launchpad.dev/sprints/rome/+addspec'
112 >>> user_browser.open(url)
113@@ -702,7 +702,8 @@
114 'http://blueprints.launchpad.dev/ubuntu/+addspec'
115 >>> for message in find_tags_by_class(user_browser.contents, 'message'):
116 ... print message.renderContents()
117- There is 1 error...already registered by another blueprint...
118+ There is 1 error...is already registered by...
119+ ...Network Magic: Auto Network Detection...
120
121
122 Registering blueprints from other locations
123
124=== modified file 'lib/lp/blueprints/stories/blueprints/xx-editing.txt'
125--- lib/lp/blueprints/stories/blueprints/xx-editing.txt 2011-03-10 01:25:13 +0000
126+++ lib/lp/blueprints/stories/blueprints/xx-editing.txt 2011-09-02 04:07:32 +0000
127@@ -6,13 +6,13 @@
128
129 First, we need to load the +edit page.
130
131- >>> browser.addHeader('Authorization', 'Basic carlos@canonical.com:test')
132- >>> features_domain = 'http://blueprints.launchpad.dev'
133- >>> spec_path = '/firefox/+spec/extension-manager-upgrades'
134- >>> browser.open(features_domain + spec_path)
135- >>> browser.getLink('Change details').click()
136- >>> browser.url
137- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades/+edit'
138+ >>> browser.addHeader('Authorization', 'Basic carlos@canonical.com:test')
139+ >>> features_domain = 'http://blueprints.launchpad.dev'
140+ >>> spec_path = '/firefox/+spec/extension-manager-upgrades'
141+ >>> browser.open(features_domain + spec_path)
142+ >>> browser.getLink('Change details').click()
143+ >>> browser.url
144+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades/+edit'
145
146 The page links back to the blueprint page, in case we change our minds.
147
148@@ -22,109 +22,110 @@
149
150 Launchpad won't let us use an URL already used in another blueprint.
151
152- >>> url = 'https://wiki.ubuntu.com/MediaIntegrityCheck'
153- >>> browser.getControl('Specification URL').value = url
154- >>> browser.getControl('Change').click()
155+ >>> url = 'https://wiki.ubuntu.com/MediaIntegrityCheck'
156+ >>> browser.getControl('Specification URL').value = url
157+ >>> browser.getControl('Change').click()
158
159- >>> message = ('https://wiki.ubuntu.com/MediaIntegrityCheck is already '
160- ... 'registered by another blueprint')
161- >>> message in browser.contents
162- True
163+ >>> message = ('https://wiki.ubuntu.com/MediaIntegrityCheck is already '
164+ ... 'registered by')
165+ >>> message in browser.contents
166+ True
167
168 Test the name validator filling a name of a existing specification for that
169 product.
170
171- >>> browser.getControl('Name').value = 'e4x'
172- >>> url = 'http://wiki.mozilla.org/Firefox:1.1_Product_Team'
173- >>> browser.getControl('Specification URL').value = url
174- >>> browser.getControl('Change').click()
175+ >>> browser.getControl('Name').value = 'e4x'
176+ >>> url = 'http://wiki.mozilla.org/Firefox:1.1_Product_Team'
177+ >>> browser.getControl('Specification URL').value = url
178+ >>> browser.getControl('Change').click()
179
180- >>> message = 'e4x is already in use by another blueprint'
181- >>> message in browser.contents
182- True
183+ >>> message = 'e4x is already in use by another blueprint'
184+ >>> message in browser.contents
185+ True
186
187 Now, let's POST the resulting changes. We should be redirected to the
188 specification home page.
189
190- >>> browser.getControl('Name').value = 'extension-manager-upgrades'
191- >>> browser.getControl('Title').value = 'Extension Manager System Upgrades'
192- >>> browser.getControl('Specification URL').value = url
193- >>> summary = ('Simplify the way extensions are installed and registered '
194- ... 'so that: 1. third party applications can easily register '
195- ... 'and deregister extensions that live with their code. 2. '
196- ... 'developers can easily register extensions that they are '
197- ... 'developing out of a location apart from their build (e.g.'
198- ... ' their home directory), and 3. developers can easily '
199- ... 'install extensions for testing.')
200- >>> browser.getControl('Summary').value = summary
201- >>> browser.getControl('Status Whiteboard').value = 'XXX'
202- >>> browser.getControl('Change').click()
203- >>> browser.url
204- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
205+ >>> browser.getControl('Name').value = 'extension-manager-upgrades'
206+ >>> browser.getControl('Title').value = (
207+ ... 'Extension Manager System Upgrades')
208+ >>> browser.getControl('Specification URL').value = url
209+ >>> summary = ('Simplify the way extensions are installed and registered '
210+ ... 'so that: 1. third party applications can easily register '
211+ ... 'and deregister extensions that live with their code. 2. '
212+ ... 'developers can easily register extensions that they are '
213+ ... 'developing out of a location apart from their build (e.g.'
214+ ... ' their home directory), and 3. developers can easily '
215+ ... 'install extensions for testing.')
216+ >>> browser.getControl('Summary').value = summary
217+ >>> browser.getControl('Status Whiteboard').value = 'XXX'
218+ >>> browser.getControl('Change').click()
219+ >>> browser.url
220+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
221
222 Also, we would like to assign these to someone other than Carlos, and we
223 would also like to have a drafter associated with it.
224
225- >>> browser.getLink(url='+people').click()
226- >>> browser.url
227- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades/+people'
228- >>> back_link = browser.getLink('Extension Manager System Upgrades')
229- >>> back_link.url
230- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
231- >>> browser.getControl('Assignee').value = 'tsukimi@quaqua.net'
232- >>> browser.getControl('Drafter').value = 'daf@canonical.com'
233- >>> browser.getControl('Approver').value = 'stuart.bishop@canonical.com'
234- >>> browser.getControl('Status Whiteboard').value = 'YYY'
235- >>> browser.getControl('Change').click()
236- >>> browser.url
237- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
238+ >>> browser.getLink(url='+people').click()
239+ >>> browser.url
240+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades/+people'
241+ >>> back_link = browser.getLink('Extension Manager System Upgrades')
242+ >>> back_link.url
243+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
244+ >>> browser.getControl('Assignee').value = 'tsukimi@quaqua.net'
245+ >>> browser.getControl('Drafter').value = 'daf@canonical.com'
246+ >>> browser.getControl('Approver').value = 'stuart.bishop@canonical.com'
247+ >>> browser.getControl('Status Whiteboard').value = 'YYY'
248+ >>> browser.getControl('Change').click()
249+ >>> browser.url
250+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
251
252 Finally, we should be able to change the status metadata (definition status,
253 implementation status, estimated man days etc) of the specification.
254
255- >>> browser.getLink(url='+status').click()
256- >>> back_link = browser.getLink('Extension Manager System Upgrades')
257- >>> back_link.url
258- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
259- >>> browser.getControl('Definition Status').value = ['DRAFT']
260- >>> # browser.getControl('Estimated Developer Days').value = '5'
261- >>> browser.getControl('Implementation Status').value = ['SLOW']
262- >>> browser.getControl('Status Whiteboard').value = 'XXX'
263- >>> browser.getControl('Change').click()
264- >>> browser.url
265- 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
266+ >>> browser.getLink(url='+status').click()
267+ >>> back_link = browser.getLink('Extension Manager System Upgrades')
268+ >>> back_link.url
269+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
270+ >>> browser.getControl('Definition Status').value = ['DRAFT']
271+ >>> # browser.getControl('Estimated Developer Days').value = '5'
272+ >>> browser.getControl('Implementation Status').value = ['SLOW']
273+ >>> browser.getControl('Status Whiteboard').value = 'XXX'
274+ >>> browser.getControl('Change').click()
275+ >>> browser.url
276+ 'http://blueprints.launchpad.dev/firefox/+spec/extension-manager-upgrades'
277
278 Any logged in user can edit a specification whiteboard.
279
280- >>> user_browser.open(
281- ... 'http://blueprints.launchpad.dev/kubuntu/'
282- ... '+spec/krunch-desktop-plan')
283- >>> user_browser.getLink(url='+whiteboard').click()
284- >>> back_link = user_browser.getLink('The Krunch Desktop Plan')
285- >>> back_link.url
286- 'http://blueprints.launchpad.dev/kubuntu/+spec/krunch-desktop-plan'
287-
288- >>> user_browser.getControl('Whiteboard').value = 'XXX by Sample Person'
289- >>> user_browser.getControl('Change').click()
290- >>> user_browser.url
291- 'http://blueprints.launchpad.dev/kubuntu/+spec/krunch-desktop-plan'
292-
293- >>> 'XXX by Sample Person' in user_browser.contents
294- True
295+ >>> user_browser.open(
296+ ... 'http://blueprints.launchpad.dev/kubuntu/'
297+ ... '+spec/krunch-desktop-plan')
298+ >>> user_browser.getLink(url='+whiteboard').click()
299+ >>> back_link = user_browser.getLink('The Krunch Desktop Plan')
300+ >>> back_link.url
301+ 'http://blueprints.launchpad.dev/kubuntu/+spec/krunch-desktop-plan'
302+
303+ >>> user_browser.getControl('Whiteboard').value = 'XXX by Sample Person'
304+ >>> user_browser.getControl('Change').click()
305+ >>> user_browser.url
306+ 'http://blueprints.launchpad.dev/kubuntu/+spec/krunch-desktop-plan'
307+
308+ >>> 'XXX by Sample Person' in user_browser.contents
309+ True
310
311 Regular users can't access the change status page.
312
313- >>> user_browser.getLink(url='+status')
314- Traceback (most recent call last):
315- ...
316- LinkNotFoundError
317+ >>> user_browser.getLink(url='+status')
318+ Traceback (most recent call last):
319+ ...
320+ LinkNotFoundError
321
322- >>> user_browser.open(
323- ... 'http://blueprints.launchpad.dev/kubuntu/'
324- ... '+spec/krunch-desktop-plan/+status')
325- Traceback (most recent call last):
326- ...
327- Unauthorized:...
328+ >>> user_browser.open(
329+ ... 'http://blueprints.launchpad.dev/kubuntu/'
330+ ... '+spec/krunch-desktop-plan/+status')
331+ Traceback (most recent call last):
332+ ...
333+ Unauthorized:...
334
335 Nor can they change a blueprint's priority.
336