Merge lp:~bac/launchpad/904335-milestone-edit into lp:launchpad

Proposed by Brad Crittenden on 2012-01-06
Status: Merged
Approved by: Curtis Hovey on 2012-01-06
Approved revision: no longer in the source branch.
Merged at revision: 14677
Proposed branch: lp:~bac/launchpad/904335-milestone-edit
Merge into: lp:launchpad
Prerequisite: lp:~bac/launchpad/904335-create-milestones-tags
Diff against target: 367 lines (+111/-22)
7 files modified
lib/lp/registry/browser/milestone.py (+30/-3)
lib/lp/registry/browser/tests/milestone-views.txt (+5/-2)
lib/lp/registry/browser/tests/test_milestone.py (+49/-5)
lib/lp/registry/javascript/milestoneoverlay.js (+11/-2)
lib/lp/registry/javascript/tests/test_milestone_creation.js (+11/-4)
lib/lp/registry/model/milestone.py (+3/-4)
lib/lp/registry/tests/test_milestonetag.py (+2/-2)
To merge this branch: bzr merge lp:~bac/launchpad/904335-milestone-edit
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code 2012-01-06 Approve on 2012-01-06
Review via email: mp+87796@code.launchpad.net

Commit Message

[r=sinzui] [r=gary_poster,jcsackett,sinzui][bug=904335] Add milestone tags.

Description of the Change

Add the tags field to the milestone +edit page.

If there is a cleaner way to insert the tags field into the form I'm
open for suggestions.

Tests:
bin/test -vv lp.registry.browser.tests.test_milestone TestMilestoneEditView

No lint.

To post a comment you must log in.
Curtis Hovey (sinzui) wrote :

Thank you for this improvement. I do not see an issue with how you add tags to the list. There are a few lines that of code that I see repeat:
+ self.field_names = self._field_names[:]
+ # Insert the tags field before the summary.
+ summary_index = self.field_names.index('summary')
+ self.field_names[summary_index:summary_index] = [tag_entry.__name__]

^ You might consider a private method or helper function so that there is one right way to add the field.

review: Approve (code)
Curtis Hovey (sinzui) wrote :

Oh, and you might find that this syntax is easier to read in a few months after your forgot what you were doing:
self.field_names.insert(summary_index, tag_entry.__name__)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/milestone.py'
2--- lib/lp/registry/browser/milestone.py 2012-01-16 00:27:33 +0000
3+++ lib/lp/registry/browser/milestone.py 2012-01-16 00:27:34 +0000
4@@ -447,7 +447,8 @@
5 # Make an instance attribute to avoid mutating the class attribute.
6 self.field_names = self.field_names[:]
7 # Insert the tags field before the summary.
8- self.field_names[3:3] = [tag_entry.__name__]
9+ summary_index = self.field_names.index('summary')
10+ self.field_names.insert(summary_index, tag_entry.__name__)
11
12 @action(_('Register Milestone'), name='register')
13 def register_action(self, action, data):
14@@ -459,7 +460,7 @@
15 summary=data.get('summary'))
16 tags = data.get('tags')
17 if tags:
18- milestone.setTags(tags.split(), self.user)
19+ milestone.setTags(tags.lower().split(), self.user)
20 self.next_url = canonical_url(self.context)
21
22 @property
23@@ -492,7 +493,7 @@
24 return canonical_url(self.context)
25
26 @property
27- def field_names(self):
28+ def _field_names(self):
29 """See `LaunchpadFormView`.
30
31 There are two series fields, one for for product milestones and the
32@@ -508,6 +509,30 @@
33 names.append('productseries')
34 return names
35
36+ @property
37+ def initial_values(self):
38+ tags = self.context.getTags()
39+ tagstring = ' '.join(tags)
40+ return dict(tags=tagstring)
41+
42+ def extendFields(self):
43+ """See `LaunchpadFormView`.
44+
45+ Add a text-entry widget for milestone tags since there is not property
46+ on the interface.
47+ """
48+ tag_entry = TextLine(
49+ __name__='tags',
50+ title=u'Tags',
51+ required=False)
52+ self.form_fields += form.Fields(
53+ tag_entry, render_context=self.render_context)
54+ # Make an instance attribute to avoid mutating the class attribute.
55+ self.field_names = self._field_names[:]
56+ # Insert the tags field before the summary.
57+ summary_index = self.field_names.index('summary')
58+ self.field_names.insert(summary_index, tag_entry.__name__)
59+
60 def setUpFields(self):
61 """See `LaunchpadFormView`.
62
63@@ -532,7 +557,9 @@
64 @action(_('Update'), name='update')
65 def update_action(self, action, data):
66 """Update the milestone."""
67+ tags = data.pop('tags') or u''
68 self.updateContextFromData(data)
69+ self.context.setTags(tags.lower().split(), self.user)
70 self.next_url = canonical_url(self.context)
71
72
73
74=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
75--- lib/lp/registry/browser/tests/milestone-views.txt 2012-01-16 00:27:33 +0000
76+++ lib/lp/registry/browser/tests/milestone-views.txt 2012-01-16 00:27:34 +0000
77@@ -347,7 +347,7 @@
78 Modify milestone details
79
80 >>> view.field_names
81- ['name', 'code_name', 'active', 'dateexpected', 'summary',
82+ ['name', 'code_name', 'active', 'dateexpected', 'tags', 'summary',
83 'productseries']
84
85 >>> print view.cancel_url
86@@ -378,6 +378,7 @@
87 ... 'field.summary': 'a summary',
88 ... 'field.active': 'False',
89 ... 'field.productseries': '1',
90+ ... 'field.tags': '',
91 ... 'field.actions.update': 'Update',
92 ... }
93 >>> view = create_initialized_view(milestone, '+edit', form=form)
94@@ -406,6 +407,7 @@
95 ... 'field.summary': 'a summary',
96 ... 'field.active': 'True',
97 ... 'field.productseries': '1',
98+ ... 'field.tags': '',
99 ... 'field.actions.update': 'Update',
100 ... }
101 >>> view = create_initialized_view(milestone, '+edit', form=form)
102@@ -443,7 +445,7 @@
103 >>> milestone = hoary_series.newMilestone('alpha')
104 >>> view = create_initialized_view(milestone, '+edit')
105 >>> view.field_names
106- ['name', 'code_name', 'active', 'dateexpected', 'summary', 'distroseries']
107+ ['name', 'code_name', 'active', 'dateexpected', 'tags', 'summary', 'distroseries']
108
109 The distroseries milestone can be updated too.
110
111@@ -454,6 +456,7 @@
112 ... 'field.summary': 'a summary',
113 ... 'field.active': 'False',
114 ... 'field.distroseries': '5',
115+ ... 'field.tags': '',
116 ... 'field.actions.update': 'Update',
117 ... }
118 >>> view = create_initialized_view(milestone, '+edit', form=form)
119
120=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
121--- lib/lp/registry/browser/tests/test_milestone.py 2012-01-16 00:27:33 +0000
122+++ lib/lp/registry/browser/tests/test_milestone.py 2012-01-16 00:27:34 +0000
123@@ -45,8 +45,8 @@
124 self.product = self.factory.makeProduct()
125 self.series = (
126 self.factory.makeProductSeries(product=self.product))
127- owner = self.product.owner
128- login_person(owner)
129+ self.owner = self.product.owner
130+ login_person(self.owner)
131
132 def test_add_milestone(self):
133 form = {
134@@ -95,7 +95,51 @@
135 self.series, '+addmilestone', form=form)
136 self.assertEqual([], view.errors)
137 expected = sorted(tags.split())
138- self.assertEqual(expected, list(self.product.milestones[0].getTags()))
139+ self.assertEqual(expected, self.product.milestones[0].getTags())
140+
141+
142+class TestMilestoneEditView(TestCaseWithFactory):
143+
144+ layer = DatabaseFunctionalLayer
145+
146+ def setUp(self):
147+ TestCaseWithFactory.setUp(self)
148+ self.product = self.factory.makeProduct()
149+ self.milestone = self.factory.makeMilestone(
150+ name='orig-name', product=self.product)
151+ self.owner = self.product.owner
152+ login_person(self.owner)
153+
154+ def test_edit_milestone_with_tags(self):
155+ orig_tags = u'b a c'
156+ self.milestone.setTags(orig_tags.split(), self.owner)
157+ new_tags = u'z a B'
158+ form = {
159+ 'field.name': 'new-name',
160+ 'field.tags': new_tags,
161+ 'field.actions.update': 'Update',
162+ }
163+ view = create_initialized_view(
164+ self.milestone, '+edit', form=form)
165+ self.assertEqual([], view.errors)
166+ self.assertEqual('new-name', self.milestone.name)
167+ expected = sorted(new_tags.lower().split())
168+ self.assertEqual(expected, self.milestone.getTags())
169+
170+ def test_edit_milestone_clear_tags(self):
171+ orig_tags = u'b a c'
172+ self.milestone.setTags(orig_tags.split(), self.owner)
173+ form = {
174+ 'field.name': 'new-name',
175+ 'field.tags': '',
176+ 'field.actions.update': 'Update',
177+ }
178+ view = create_initialized_view(
179+ self.milestone, '+edit', form=form)
180+ self.assertEqual([], view.errors)
181+ self.assertEqual('new-name', self.milestone.name)
182+ expected = []
183+ self.assertEqual(expected, self.milestone.getTags())
184
185
186 class TestMilestoneMemcache(MemcacheTestCase):
187@@ -475,7 +519,7 @@
188 def _make_form(self, tags):
189 return {
190 u'field.actions.search': u'Search',
191- u'field.tags': u' '.join(tags)
192+ u'field.tags': u' '.join(tags),
193 }
194
195 def _url_tail(self, url, separator='/'):
196@@ -508,7 +552,7 @@
197 self.assertEqual(1, len(view.errors))
198 self.assertEqual('tags', view.errors[0].field_name)
199
200- def test_buktask_query_count(self):
201+ def test_bugtask_query_count(self):
202 # Ensure that a correct number of queries is executed for
203 # bugtasks retrieval.
204 bugtask_count = 10
205
206=== modified file 'lib/lp/registry/javascript/milestoneoverlay.js'
207--- lib/lp/registry/javascript/milestoneoverlay.js 2012-01-16 00:27:33 +0000
208+++ lib/lp/registry/javascript/milestoneoverlay.js 2012-01-16 00:27:34 +0000
209@@ -66,7 +66,11 @@
210 error_handler.getFailureHandler()(
211 ioid, response, args);
212 },
213- failure: error_handler_nested.getFailureHandler()
214+ failure: function(ioid, response, args) {
215+ module.last_error = response;
216+ error_handler_nested.getFailureHandler()
217+ (ioid, response, args);
218+ }
219 }
220 };
221 lp_client.io_provider.io(milestone.get('self_link'), config);
222@@ -93,6 +97,7 @@
223 on: {
224 success: finish_new_milestone,
225 failure: function(ioid, response, args) {
226+ module.last_error = response;
227 delete_new_milestone(
228 milestone, ioid, response, args);
229 }
230@@ -105,7 +110,11 @@
231 parameters: parameters,
232 on: {
233 success: set_milestone_tags,
234- failure: error_handler.getFailureHandler()
235+ failure: function(ioid, response, args) {
236+ module.last_error = response;
237+ error_handler.getFailureHandler()
238+ (ioid, response, args);
239+ }
240 }
241 });
242 };
243
244=== modified file 'lib/lp/registry/javascript/tests/test_milestone_creation.js'
245--- lib/lp/registry/javascript/tests/test_milestone_creation.js 2012-01-16 00:27:33 +0000
246+++ lib/lp/registry/javascript/tests/test_milestone_creation.js 2012-01-16 00:27:34 +0000
247@@ -50,6 +50,7 @@
248 test_configure: function() {
249 // Ensure configuring the module works as it will be needed in
250 // subsequent tests.
251+ var client = new Y.lp.client.Launchpad({sync: true});
252 var config = {
253 milestone_form_uri: 'a',
254 series_uri: 'b',
255@@ -62,7 +63,6 @@
256 // Setup the fixture, retrieving the objects we need for the test.
257 var data = serverfixture.setup(this, 'setup');
258 var client = new Y.lp.client.Launchpad({sync: true});
259- var config = makeTestConfig();
260 var product = new Y.lp.client.Entry(
261 client, data.product, data.product.self_link);
262 var product_name = product.get('name');
263@@ -95,10 +95,12 @@
264 };
265
266 // Test the creation of the new milestone.
267+ the_module.last_error = null;
268 the_module.save_new_milestone(params);
269+ Y.Assert.isNull(the_module.last_error,
270+ "last_error is: " + the_module.last_error);
271
272 // Verify the milestone was created.
273- var client = new Y.lp.client.Launchpad({sync: true});
274 var product = new Y.lp.client.Entry(
275 client, data.product, data.product.self_link);
276 config = makeTestConfig({parameters: {name: milestone_name}});
277@@ -106,7 +108,8 @@
278 product.named_get('getMilestone', config);
279 Y.Assert.isTrue(config.successful, 'Getting milestone failed');
280 var milestone = config.result;
281- Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
282+ Y.Assert.isInstanceOf(
283+ Y.lp.client.Entry, milestone, 'Milestone is not an Entry');
284 Y.Assert.areEqual(milestone_name, milestone.get('name'));
285 Y.Assert.areEqual(code_name, milestone.get('code_name'));
286 // Ensure no tags are created.
287@@ -144,7 +147,10 @@
288 };
289
290 // Test the creation of the new milestone.
291+ the_module.last_error = null;
292 the_module.save_new_milestone(params);
293+ Y.Assert.isNull(the_module.last_error,
294+ "last_error is: " + the_module.last_error);
295
296 // Verify the milestone was created.
297 var product = new Y.lp.client.Entry(
298@@ -154,7 +160,8 @@
299 product.named_get('getMilestone', config);
300 Y.Assert.isTrue(config.successful, 'Getting milestone failed');
301 var milestone = config.result;
302- Y.Assert.isInstanceOf(Y.lp.client.Entry, milestone);
303+ Y.Assert.isInstanceOf(
304+ Y.lp.client.Entry, milestone, 'Milestone is not an Entry');
305 Y.Assert.areEqual(milestone_name, milestone.get('name'));
306 Y.Assert.areEqual(code_name, milestone.get('code_name'));
307 // Ensure the tags are created.
308
309=== modified file 'lib/lp/registry/model/milestone.py'
310--- lib/lp/registry/model/milestone.py 2012-01-16 00:27:33 +0000
311+++ lib/lp/registry/model/milestone.py 2012-01-16 00:27:34 +0000
312@@ -301,22 +301,21 @@
313 else:
314 store.find(
315 MilestoneTag, MilestoneTag.milestone_id == self.id).remove()
316- store.commit()
317
318 def getTagsData(self):
319 """See IMilestone."""
320 # Prevent circular references.
321 from lp.registry.model.milestonetag import MilestoneTag
322 store = Store.of(self)
323- return list(store.find(
324+ return store.find(
325 MilestoneTag, MilestoneTag.milestone_id == self.id
326- ).order_by(MilestoneTag.tag))
327+ ).order_by(MilestoneTag.tag)
328
329 def getTags(self):
330 """See IMilestone."""
331 # Prevent circular references.
332 from lp.registry.model.milestonetag import MilestoneTag
333- return self.getTagsData().values(MilestoneTag.tag)
334+ return list(self.getTagsData().values(MilestoneTag.tag))
335
336
337 class MilestoneSet:
338
339=== modified file 'lib/lp/registry/tests/test_milestonetag.py'
340--- lib/lp/registry/tests/test_milestonetag.py 2012-01-16 00:27:33 +0000
341+++ lib/lp/registry/tests/test_milestonetag.py 2012-01-16 00:27:34 +0000
342@@ -5,8 +5,8 @@
343
344 __metaclass__ = type
345
346+import datetime
347 import transaction
348-import datetime
349
350 from lp.testing.layers import (
351 AppServerLayer,
352@@ -20,7 +20,6 @@
353 person_logged_in,
354 TestCaseWithFactory,
355 WebServiceTestCase,
356- ws_object,
357 )
358
359
360@@ -225,6 +224,7 @@
361 def test_get_tags(self):
362 tags = [u'zeta', u'alpha', u'beta']
363 self.milestone.setTags(tags, self.owner)
364+ transaction.commit()
365 self.assertEqual(sorted(tags), self.ws_milestone.getTags())
366
367 def test_set_tags_initial(self):