Merge lp:~kfogel/launchpad/505845-affects-person-from-dups into lp:launchpad

Proposed by Karl Fogel on 2010-01-20
Status: Merged
Approved by: Graham Binns on 2010-01-20
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~kfogel/launchpad/505845-affects-person-from-dups
Merge into: lp:launchpad
Diff against target: 308 lines (+219/-5)
5 files modified
lib/lp/bugs/configure.zcml (+3/-0)
lib/lp/bugs/doc/bug.txt (+145/-1)
lib/lp/bugs/interfaces/bug.py (+22/-2)
lib/lp/bugs/model/bug.py (+46/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+3/-0)
To merge this branch: bzr merge lp:~kfogel/launchpad/505845-affects-person-from-dups
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2010-01-20 Approve on 2010-01-20
Karl Fogel (community) Resubmit on 2010-01-20
Review via email: mp+17721@code.launchpad.net
To post a comment you must log in.
Karl Fogel (kfogel) wrote :

Fix bug #505845 by adding new properties Bug.users_affected_with_dupes and Bug.users_affected_count_with_dupes, so bug heat can do the correct calculations, and (eventually) so the web UI can show more accurate information.

Along the way, fix a long-standing bug in Bug.users_affected whereby it was counting users who had explicitly said "does not affect me" as being affected.

Note the one "XXX" still in the change.

Graham Binns (gmb) wrote :
Download full text (11.5 KiB)

Nice branch! Couple of nitpicks and a question about your XXX, but
otherwise nothing major. r=me with the fix for the XXX applied if
possible (or a promise that it will land shortly after this branch
does).

> === modified file 'lib/lp/bugs/doc/bug.txt'
> --- lib/lp/bugs/doc/bug.txt 2009-12-12 09:46:03 +0000
> +++ lib/lp/bugs/doc/bug.txt 2010-01-20 07:25:26 +0000
> @@ -1242,12 +1242,18 @@
> 0
> >>> test_bug.users_affected_count
> 3
> + >>> test_bug.markUserAffected(unaffected_user, affected=False)
> + >>> test_bug.isUserAffected(unaffected_user)
> + False
> + >>> test_bug.users_unaffected_count
> + 1
> + >>> test_bug.users_affected_count
> + 2
>
> We can also get the collection of users affected by a bug.
>
> >>> print '\n'.join(
> ... sorted(user.name for user in test_bug.users_affected))
> - blaze-bayley
> bruce-dickinson
> paul-dianno
>
> @@ -1255,6 +1261,144 @@
> >>> print list(unaffecting_bug.users_affected)
> [<Person at ...>]
>
> +Similarly, we can get the collection of users unaffected by a bug.
> +
> + >>> print '\n'.join(
> + ... sorted(user.name for user in test_bug.users_unaffected))
> + blaze-bayley
> +
> +If a user is marked as being affected by a bug (either by explicitly
> +marking it so, or by being the bug's owner), and then that bug is
> +marked as a duplicate of master bug, then the users_affected_count of
> +the master bug increases too.
> +
> + >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
> + >>> dupe_one = factory.makeBug(owner=dupe_affected_user)
> + >>> dupe_one.duplicateof = test_bug
> + >>> test_bug.users_affected_count_with_dupes
> + 3
> +
> +And the list of users the master bug affects includes that user.
> +
> + >>> print '\n'.join(
> + ... sorted(user.name for user in test_bug.users_affected_with_dupes))
> + bruce-dickinson
> + paul-dianno
> + sheila-shakespeare
> +
> +However, if the user was also marked as being affected by the master
> +bug, then the master bug's user_affected_count does *not* increment
> +just because she is also affected by the duplicate.
> +
> + >>> test_bug.markUserAffected(dupe_affected_user, affected=True)
> + >>> test_bug.users_affected_count_with_dupes
> + 3
> +
> +And the list of users that the master bug affects still includes the
> +user, of course.
> +
> + >>> print '\n'.join(
> + ... sorted(user.name for user in test_bug.users_affected_with_dupes))
> + bruce-dickinson
> + paul-dianno
> + sheila-shakespeare
> +
> +If there is another dup of the master bug, filed by someone else, the
> +master bug's affected count with dups increases.
> +
> + >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
> + >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
> + >>> dupe_three.duplicateof = test_bug
> + >>> test_bug.users_affected_count_with_dupes
> + 4
> +
> +If the user claims that two bugs both affect her, then if they are
> +both marked as duplicates of the master bugs, the master bug's
> +user_affected_count still only increments by 1 for that user.
> +
...

review: Approve (code)
Karl Fogel (kfogel) wrote :

Fixed typos, and took care of "XXX" comment with a surprisingly simple solution (well, surprising to me anyway).

review: Resubmit
Graham Binns (gmb) wrote :

Looks awesome.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/configure.zcml'
2--- lib/lp/bugs/configure.zcml 2010-01-20 20:19:05 +0000
3+++ lib/lp/bugs/configure.zcml 2010-01-22 23:54:16 +0000
4@@ -629,6 +629,9 @@
5 users_unaffected_count
6 readonly_duplicateof
7 users_affected
8+ users_unaffected
9+ users_affected_count_with_dupes
10+ users_affected_with_dupes
11 bug_messages
12 isUserAffected
13 getHWSubmissions"/>
14
15=== modified file 'lib/lp/bugs/doc/bug.txt'
16--- lib/lp/bugs/doc/bug.txt 2009-12-12 09:46:03 +0000
17+++ lib/lp/bugs/doc/bug.txt 2010-01-22 23:54:16 +0000
18@@ -1242,12 +1242,18 @@
19 0
20 >>> test_bug.users_affected_count
21 3
22+ >>> test_bug.markUserAffected(unaffected_user, affected=False)
23+ >>> test_bug.isUserAffected(unaffected_user)
24+ False
25+ >>> test_bug.users_unaffected_count
26+ 1
27+ >>> test_bug.users_affected_count
28+ 2
29
30 We can also get the collection of users affected by a bug.
31
32 >>> print '\n'.join(
33 ... sorted(user.name for user in test_bug.users_affected))
34- blaze-bayley
35 bruce-dickinson
36 paul-dianno
37
38@@ -1255,6 +1261,144 @@
39 >>> print list(unaffecting_bug.users_affected)
40 [<Person at ...>]
41
42+Similarly, we can get the collection of users unaffected by a bug.
43+
44+ >>> print '\n'.join(
45+ ... sorted(user.name for user in test_bug.users_unaffected))
46+ blaze-bayley
47+
48+If a user is marked as being affected by a bug (either by explicitly
49+marking it so, or by being the bug's owner), and then that bug is
50+marked as a duplicate of master bug, then the users_affected_count of
51+the master bug increases too.
52+
53+ >>> dupe_affected_user = factory.makePerson(name='sheila-shakespeare')
54+ >>> dupe_one = factory.makeBug(owner=dupe_affected_user)
55+ >>> dupe_one.duplicateof = test_bug
56+ >>> test_bug.users_affected_count_with_dupes
57+ 3
58+
59+And the list of users the master bug affects includes that user.
60+
61+ >>> print '\n'.join(
62+ ... sorted(user.name for user in test_bug.users_affected_with_dupes))
63+ bruce-dickinson
64+ paul-dianno
65+ sheila-shakespeare
66+
67+However, if the user was also marked as being affected by the master
68+bug, then the master bug's user_affected_count does *not* increment
69+just because she is also affected by the duplicate.
70+
71+ >>> test_bug.markUserAffected(dupe_affected_user, affected=True)
72+ >>> test_bug.users_affected_count_with_dupes
73+ 3
74+
75+And the list of users that the master bug affects still includes the
76+user, of course.
77+
78+ >>> print '\n'.join(
79+ ... sorted(user.name for user in test_bug.users_affected_with_dupes))
80+ bruce-dickinson
81+ paul-dianno
82+ sheila-shakespeare
83+
84+If there is another dup of the master bug, filed by someone else, the
85+master bug's affected count with dups increases.
86+
87+ >>> dupe_affected_other_user = factory.makePerson(name='napoleon-bonaparte')
88+ >>> dupe_three = factory.makeBug(owner=dupe_affected_other_user)
89+ >>> dupe_three.duplicateof = test_bug
90+ >>> test_bug.users_affected_count_with_dupes
91+ 4
92+
93+If the user claims that two bugs both affect her, then if they are
94+both marked as duplicates of the master bugs, the master bug's
95+user_affected_count still only increments by 1 for that user.
96+
97+ >>> dupe_two = factory.makeBug(owner=dupe_affected_user)
98+ >>> dupe_two.duplicateof = test_bug
99+ >>> test_bug.users_affected_count_with_dupes
100+ 4
101+
102+Both duplicates claim to affect just that user:
103+
104+ >>> print '\n'.join(
105+ ... sorted(user.name for user in dupe_one.users_affected))
106+ sheila-shakespeare
107+ >>> print '\n'.join(
108+ ... sorted(user.name for user in dupe_two.users_affected))
109+ sheila-shakespeare
110+
111+And the list of users that the master bug affects includes the user
112+exactly once, of course.
113+
114+ >>> print '\n'.join(
115+ ... sorted(user.name for user in test_bug.users_affected_with_dupes))
116+ bruce-dickinson
117+ napoleon-bonaparte
118+ paul-dianno
119+ sheila-shakespeare
120+
121+If the user marks the master bug as not affecting her, but the master
122+bug still has a duplicate that she claims affects her, then that
123+duplicate is also marked as not affecting her either.
124+
125+ >>> dupe_one.users_affected_count
126+ 1
127+ >>> test_bug.markUserAffected(dupe_affected_user, affected=False)
128+ >>> dupe_one.users_affected_count
129+ 0
130+
131+The master bug's affected count, with or without dups, is reduced by one:
132+
133+ >>> test_bug.users_affected_count
134+ 2
135+ >>> test_bug.users_affected_count_with_dupes
136+ 3
137+
138+The dup user no longer appears as affected by the master bug nor
139+either of the dups:
140+
141+ >>> print '\n'.join(
142+ ... sorted(user.name for user in test_bug.users_affected_with_dupes))
143+ bruce-dickinson
144+ napoleon-bonaparte
145+ paul-dianno
146+ >>> print '\n'.join(
147+ ... sorted(user.name for user in dupe_one.users_affected))
148+ <BLANKLINE>
149+ >>> print '\n'.join(
150+ ... sorted(user.name for user in dupe_two.users_affected))
151+ <BLANKLINE>
152+
153+Since the user who filed the first two dups had an entry explicitly
154+saying she was affected, they now claim that she is unaffected.
155+
156+ >>> print '\n'.join(
157+ ... sorted(user.name for user in dupe_one.users_unaffected))
158+ sheila-shakespeare
159+ >>> print '\n'.join(
160+ ... sorted(user.name for user in dupe_two.users_unaffected))
161+ sheila-shakespeare
162+
163+But she didn't file the third dup, so there was never any explicit
164+record saying she was affected by it. Thus she also does not appear
165+as explicitly unaffected, even after marking the master bug as not
166+affecting her.
167+
168+ >>> print '\n'.join(
169+ ... sorted(user.name for user in dupe_three.users_unaffected))
170+ <BLANKLINE>
171+
172+However, if a dup was not marked either way for that user, then do
173+nothing to the dup when the master is marked as not affecting the
174+user.
175+
176+ >>> print '\n'.join(
177+ ... sorted(user.name for user in dupe_three.users_affected))
178+ napoleon-bonaparte
179+
180
181 Getting the distinct set of Bugs for a set of BugTasks
182 ------------------------------------------------------
183
184=== modified file 'lib/lp/bugs/interfaces/bug.py'
185--- lib/lp/bugs/interfaces/bug.py 2010-01-20 20:19:05 +0000
186+++ lib/lp/bugs/interfaces/bug.py 2010-01-22 23:54:16 +0000
187@@ -286,13 +286,33 @@
188 title=_('The number of comments on this bug'),
189 required=True, readonly=True)
190 users_affected_count = exported(
191- Int(title=_('The number of users affected by this bug'),
192+ Int(title=_('The number of users affected by this bug '
193+ '(not including duplicates)'),
194 required=True, readonly=True))
195 users_unaffected_count = exported(
196+ # We don't say "(not including duplicates)" here because
197+ # affected and unaffected are asymmetrical that way. If a dup
198+ # affects you, then the master bug affects you; but if a dup
199+ # *doesn't* affect you, the master bug may or may not affect
200+ # you, since a dup is often a specific symptom of a more
201+ # general master bug.
202 Int(title=_('The number of users unaffected by this bug'),
203 required=True, readonly=True))
204 users_affected = exported(CollectionField(
205- title=_('Users affected'),
206+ title=_('Users affected (not including duplicates)'),
207+ value_type=Reference(schema=IPerson),
208+ readonly=True))
209+ users_unaffected = exported(CollectionField(
210+ title=_('Users explicitly marked as unaffected '
211+ '(not including duplicates)'),
212+ value_type=Reference(schema=IPerson),
213+ readonly=True))
214+ users_affected_count_with_dupes = exported(
215+ Int(title=_('The number of users affected by this bug '
216+ '(including duplicates)'),
217+ required=True, readonly=True))
218+ users_affected_with_dupes = exported(CollectionField(
219+ title=_('Users affected (including duplicates)'),
220 value_type=Reference(schema=IPerson),
221 readonly=True))
222
223
224=== modified file 'lib/lp/bugs/model/bug.py'
225--- lib/lp/bugs/model/bug.py 2010-01-22 17:31:00 +0000
226+++ lib/lp/bugs/model/bug.py 2010-01-22 23:54:16 +0000
227@@ -31,7 +31,8 @@
228 from sqlobject import BoolCol, IntCol, ForeignKey, StringCol
229 from sqlobject import SQLMultipleJoin, SQLRelatedJoin
230 from sqlobject import SQLObjectNotFound
231-from storm.expr import And, Count, In, LeftJoin, Select, SQLRaw, Func
232+from storm.expr import (
233+ And, Count, Func, In, LeftJoin, Not, Select, SQLRaw, Union)
234 from storm.store import EmptyResultSet, Store
235
236 from lazr.lifecycle.event import (
237@@ -265,7 +266,46 @@
238 """See `IBug`."""
239 return Store.of(self).find(
240 Person, BugAffectsPerson.person == Person.id,
241- BugAffectsPerson.bug == self)
242+ BugAffectsPerson.affected,
243+ BugAffectsPerson.bug == self)
244+
245+ @property
246+ def users_unaffected(self):
247+ """See `IBug`."""
248+ return Store.of(self).find(
249+ Person, BugAffectsPerson.person == Person.id,
250+ Not(BugAffectsPerson.affected),
251+ BugAffectsPerson.bug == self)
252+
253+ @property
254+ def user_ids_affected_with_dupes(self):
255+ """Return all IDs of Persons affected by this bug and its dupes.
256+ The return value is a Storm expression. Running a query with
257+ this expression returns a result that may contain the same ID
258+ multiple times, for example if that person is affected via
259+ more than one duplicate."""
260+ return Union(
261+ Select(Person.id,
262+ And(BugAffectsPerson.person == Person.id,
263+ BugAffectsPerson.affected,
264+ BugAffectsPerson.bug == self)),
265+ Select(Person.id,
266+ And(BugAffectsPerson.person == Person.id,
267+ BugAffectsPerson.bug == Bug.id,
268+ BugAffectsPerson.affected,
269+ Bug.duplicateof == self.id)))
270+
271+ @property
272+ def users_affected_with_dupes(self):
273+ """See `IBug`."""
274+ return Store.of(self).find(
275+ Person,
276+ In(Person.id, self.user_ids_affected_with_dupes))
277+
278+ @property
279+ def users_affected_count_with_dupes(self):
280+ """See `IBug`."""
281+ return self.users_affected_with_dupes.count()
282
283 @property
284 def indexed_messages(self):
285@@ -1348,6 +1388,10 @@
286 if bap.affected != affected:
287 bap.affected = affected
288 self._flushAndInvalidate()
289+ # Loop over dupes.
290+ for dupe in self.duplicates:
291+ if dupe._getAffectedUser(user) is not None:
292+ dupe.markUserAffected(user, affected)
293
294 @property
295 def readonly_duplicateof(self):
296
297=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
298--- lib/lp/bugs/stories/webservice/xx-bug.txt 2009-11-20 04:21:24 +0000
299+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2010-01-22 23:54:16 +0000
300@@ -42,6 +42,9 @@
301 title: u'Make Jokosher use autoaudiosink'
302 users_affected_collection_link: u'http://.../bugs/11/users_affected'
303 users_affected_count: 0
304+ users_affected_count_with_dupes: 0
305+ users_affected_with_dupes_collection_link: u'http://.../bugs/11/users_affected_with_dupes'
306+ users_unaffected_collection_link: u'http://.../bugs/11/users_unaffected'
307 users_unaffected_count: 0
308 who_made_private_link: None
309