Merge lp:~dooferlad/launchpad/workitems-bugfix into lp:launchpad

Proposed by James Tunnicliffe
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 15445
Proposed branch: lp:~dooferlad/launchpad/workitems-bugfix
Merge into: lp:launchpad
Diff against target: 122 lines (+78/-10)
2 files modified
lib/lp/blueprints/model/specification.py (+27/-10)
lib/lp/blueprints/model/tests/test_specification.py (+51/-0)
To merge this branch: bzr merge lp:~dooferlad/launchpad/workitems-bugfix
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+110563@code.launchpad.net

Commit message

Bug Fix for bug #1004416. Work item editor now handles duplicate work items correctly.

Description of the change

-- Summary
Bug Fix for #1004416
The existing work item editor code assumed that work items would be uniquely identifiable by title alone. When inserting a new work item you could insert a duplicate, but any changes to it would only apply to the first work item that the code found. Deleting one of those work items would fail - you had to delete both.

-- Proposed fix
Modify logic that takes work item updates to not just use work item name as the only identifier.

-- Pre-implementation notes
None.

-- Implementation details
Since the work item editor is still a text box, the work item name is still used to identify a work item, but the code takes care to only match a work item for update or deletion once. This resolves the problems seen.

-- LOC Rationale
Added quite a few lines, though. These will be more than offset by:
https://code.launchpad.net/~danilo/launchpad/kill-feedback-requests/+merge/106119

-- Tests
test_duplicate_work_items in lib/lp/blueprints/model/tests/test_specificatin.py

-- Demo and Q/A
1. In a dev instance run http://paste.ubuntu.com/992291/ to generate some work items
2. Visit https://blueprints.launchpad.dev/linaro/+spec/name-100098 and create a copy of the last work item. Previously this wouldn’t work. Now delete it. It should be deleted. Previously you had to delete all work items with the same name.

-- Lint
None.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi James,

Thanks for making this change. I hadn't paid a lot of attention to the new work items feature of blueprints so it was interesting to learn a little about them.

* The logic to populate a dictionary with the count of items in a list is pretty generic and repeated. Perhaps you could factor that out into a method.

* Please change "if(" to "if (" at line 17 and elsewhere.

* I think "if not title in existing_title_count" reads better as "if title not in existing_title_count", as you'd written it previously.

* In the following

   if(new_wi['title'] not in existing_titles or
      new_wi['title'] in existing_title_count and
      existing_title_count[new_wi['title']] == 0):

the second line is redundant as you just established the title is in existing_titles so it must also appear in existing_title_count.

* I know you didn't write this line, but

for i in range(len(new_work_items)):
    new_wi = new_work_items[i]

would be easier to follow if it was done like

for i, new_wi in enumerate(new_work_items):

* You have a typo "anotehr".

* In your test, it might be easier to understand if your duplicate items were copied from the first one, i.e.

from copy import copy
new_w1_data = dict(...)
new_w2_data = copy(new_w1_data)
...
new_w3_data = copy(new_w1_data)

A reader will know instantly what is going on without having to look at every item in the dicts to verify they are the same.

Finally I'll note this test is very busy. Ideally each test would exercise one concept. Perhaps you could break this test up into one that shows adding duplicates works and another that proves deleting will work. That separation comes at the price of refactoring or (worse) duplicate set up code.

I'd like to go ahead and mark this proposal 'Approved', pending the mentioned changes being made.

Thanks for the nice branch.

review: Approve (code)
Revision history for this message
James Tunnicliffe (dooferlad) wrote :

I have pushed a change that addresses these concerns. I don't have commit access, so please kick off the test and check in cycle.

Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the changes James. I'll land this branch for you now.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/model/specification.py'
2--- lib/lp/blueprints/model/specification.py 2012-05-17 07:46:56 +0000
3+++ lib/lp/blueprints/model/specification.py 2012-06-18 11:22:27 +0000
4@@ -289,10 +289,25 @@
5
6 Also set the sequence of those deleted work items to -1.
7 """
8+ title_counts = self._list_to_dict_of_frequency(titles)
9+
10 for work_item in self.work_items:
11- if work_item.title not in titles:
12+ if (work_item.title not in title_counts or
13+ title_counts[work_item.title] == 0):
14 work_item.deleted = True
15
16+ elif title_counts[work_item.title] > 0:
17+ title_counts[work_item.title] -= 1
18+
19+ def _list_to_dict_of_frequency(self, list):
20+ dictionary = {}
21+ for item in list:
22+ if not item in dictionary:
23+ dictionary[item] = 1
24+ else:
25+ dictionary[item] += 1
26+ return dictionary
27+
28 def updateWorkItems(self, new_work_items):
29 """See ISpecification."""
30 # First mark work items with titles that are no longer present as
31@@ -308,18 +323,20 @@
32 # ones.
33 to_insert = []
34 existing_titles = [wi.title for wi in work_items]
35- for i in range(len(new_work_items)):
36- new_wi = new_work_items[i]
37- if new_wi['title'] not in existing_titles:
38- # This is a new work item, so we insert it with 'i' as its
39- # sequence because that's the position it is on the list
40- # entered by the user.
41+ existing_title_count = self._list_to_dict_of_frequency(existing_titles)
42+
43+ for i, new_wi in enumerate(new_work_items):
44+ if (new_wi['title'] not in existing_titles or
45+ existing_title_count[new_wi['title']] == 0):
46 to_insert.append((i, new_wi))
47 else:
48- # Get the existing work item with the same title and update
49+ existing_title_count[new_wi['title']] -= 1
50+ # Get an existing work item with the same title and update
51 # it to match what we have now.
52- existing_wi = work_items[
53- existing_titles.index(new_wi['title'])]
54+ existing_wi_index = existing_titles.index(new_wi['title'])
55+ existing_wi = work_items[existing_wi_index]
56+ # Mark a work item as dirty - don't use it again this update.
57+ existing_titles[existing_wi_index] = None
58 # Update the sequence to match its current position on the
59 # list entered by the user.
60 existing_wi.sequence = i
61
62=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
63--- lib/lp/blueprints/model/tests/test_specification.py 2012-03-26 14:23:39 +0000
64+++ lib/lp/blueprints/model/tests/test_specification.py 2012-06-18 11:22:27 +0000
65@@ -494,6 +494,57 @@
66 for data, obj in zip(work_items, list(spec.work_items)):
67 self.assertThat(obj, MatchesStructure.byEquality(**data))
68
69+ def _dup_work_items_set_up(self):
70+ spec = self.factory.makeSpecification(
71+ product=self.factory.makeProduct())
72+ login_person(spec.owner)
73+ # Create two work-items in our database.
74+ wi1_data = self._createWorkItemAndReturnDataDict(spec)
75+ wi2_data = self._createWorkItemAndReturnDataDict(spec)
76+
77+ # Create a duplicate and a near duplicate, insert into DB.
78+ new_wi1_data = wi2_data.copy()
79+ new_wi2_data = new_wi1_data.copy()
80+ new_wi2_data['status'] = SpecificationWorkItemStatus.DONE
81+ work_items = [new_wi1_data, wi1_data, new_wi2_data, wi2_data]
82+ spec.updateWorkItems(work_items)
83+
84+ # Update our data dicts with the sequences to match data in DB
85+ new_wi1_data['sequence'] = 0
86+ wi1_data['sequence'] = 1
87+ new_wi2_data['sequence'] = 2
88+ wi2_data['sequence'] = 3
89+
90+ self.assertEqual(4, spec.work_items.count())
91+ for data, obj in zip(work_items, list(spec.work_items)):
92+ self.assertThat(obj, MatchesStructure.byEquality(**data))
93+
94+ return spec, work_items
95+
96+ def test_add_duplicate_work_item(self):
97+ spec, work_items = self._dup_work_items_set_up()
98+
99+ # Test that we can insert another duplicate work item.
100+ new_wi3_data = work_items[0].copy()
101+ new_wi3_data['sequence'] = 4
102+ work_items.append(new_wi3_data)
103+ spec.updateWorkItems(work_items)
104+
105+ self.assertEqual(5, spec.work_items.count())
106+ for data, obj in zip(work_items, list(spec.work_items)):
107+ self.assertThat(obj, MatchesStructure.byEquality(**data))
108+
109+ def test_delete_duplicate_work_item(self):
110+ spec, work_items = self._dup_work_items_set_up()
111+
112+ # Delete a duplicate work item
113+ work_items.pop()
114+ spec.updateWorkItems(work_items)
115+
116+ self.assertEqual(3, spec.work_items.count())
117+ for data, obj in zip(work_items, list(spec.work_items)):
118+ self.assertThat(obj, MatchesStructure.byEquality(**data))
119+
120 def test_updateWorkItems_updates_existing_ones(self):
121 spec = self.factory.makeSpecification()
122 login_person(spec.owner)