Merge lp:~mbp/launchpad/678090-affected-count into lp:launchpad

Proposed by Martin Pool
Status: Merged
Approved by: Martin Pool
Approved revision: no longer in the source branch.
Merged at revision: 14292
Proposed branch: lp:~mbp/launchpad/678090-affected-count
Merge into: lp:launchpad
Diff against target: 362 lines (+184/-43)
7 files modified
lib/lp/bugs/browser/bugtask.py (+44/-27)
lib/lp/bugs/browser/tests/test_bugtask.py (+112/-9)
lib/lp/bugs/configure.zcml (+1/-0)
lib/lp/bugs/interfaces/bug.py (+11/-7)
lib/lp/bugs/model/bug.py (+9/-0)
lib/lp/bugs/stories/webservice/xx-bug.txt (+1/-0)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~mbp/launchpad/678090-affected-count
Reviewer Review Type Date Requested Status
Ian Booth Pending
Review via email: mp+81108@code.launchpad.net

Commit message

[r=mbp][bug=678090] bug page shows count of all affected people across dupes

Description of the change

This changes the "affects N people" on a bug page to show the total people affected across all bugs.

It does this by actually counting them, which may or may not be a performance problem. I have checked there is only one query, and actually by rearranging the code and using a cachedproperty it now does fewer queries than previously.

Robert suggested just adding up the cached affected-user-count across all duplicates, but that's incorrect in a way that's likely to generate knock-on bugs, and it wasn't obvious how to implement it.

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

This branch is currently incomplete but I'd still appreciate pre-review on the general approach. Specifically before it can land:

 * it needs the javascript version updated
 * perhaps the js and server-side forms can be unified using Moustache?
 * it needs tests

Revision history for this message
Robert Collins (lifeless) wrote :

This looks wrong:
38 + if total_affected == 1:
39 + return "This bug affects you and 1 other person"

surely thats 'This bug affects you', and a count of 2 is you and 1 other person, and 3+ is you and %d other people.

I think the approach is reasonable but you should consider feature flagging it in case it is slower. Also changing templates etc would just messy up the clarity of this change.

Revision history for this message
Martin Pool (mbp) wrote :

On 3 November 2011 15:59, Robert Collins <email address hidden> wrote:
> This looks wrong:
> 38      + if total_affected == 1:
> 39      + return "This bug affects you and 1 other person"
>
> surely thats 'This bug affects you', and a count of 2 is you and 1 other person, and 3+ is you and %d other people.

thanks.

>
> I think the approach is reasonable but you should consider feature flagging it in case it is slower.

good idea, and that will actually give good assurance that it's no
slower when the feature is off.

> Also changing templates etc would just messy up the clarity of this change.

k

Revision history for this message
Martin Pool (mbp) wrote :

This adds a feature flag checked on the view. It will cost us one more query, so the query count tests are pushed up, but only once per view.

Also some decent tests.

It turns out if I override the view properly this does not need js changes, which is nice.

Revision history for this message
Martin Pool (mbp) wrote :

I've added a flag, more tests, and made it more correct in cases like the current user being affected by a dupe. Please re-review.

Revision history for this message
Martin Pool (mbp) wrote :

<wallyworld_> poolie: looks ok to me, my misreading of the fflag aside. my personal preference is not to set the flag if the test is expecting the default behaviour

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugtask.py'
2--- lib/lp/bugs/browser/bugtask.py 2011-11-11 18:36:39 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-11-14 05:10:29 +0000
4@@ -3626,59 +3626,76 @@
5 else:
6 return 'false'
7
8- @property
9+ @cachedproperty
10 def other_users_affected_count(self):
11- """The number of other users affected by this bug."""
12- if self.current_user_affected_status:
13- return self.context.users_affected_count - 1
14+ """The number of other users affected by this bug.
15+ """
16+ if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
17+ if self.current_user_affected_status:
18+ return self.context.users_affected_count - 1
19+ else:
20+ return self.context.users_affected_count
21 else:
22+ return self.context.other_users_affected_count_with_dupes
23+
24+ @cachedproperty
25+ def total_users_affected_count(self):
26+ """The number of affected users, typically across all users.
27+
28+ Counting across duplicates may be disabled at run time.
29+ """
30+ if getFeatureFlag('bugs.affected_count_includes_dupes.disabled'):
31 return self.context.users_affected_count
32+ else:
33+ return self.context.users_affected_count_with_dupes
34
35- @property
36+ @cachedproperty
37 def affected_statement(self):
38 """The default "this bug affects" statement to show.
39
40 The outputs of this method should be mirrored in
41 MeTooChoiceSource._getSourceNames() (Javascript).
42 """
43- if self.other_users_affected_count == 1:
44- if self.current_user_affected_status is None:
45+ me_affected = self.current_user_affected_status
46+ other_affected = self.other_users_affected_count
47+ if me_affected is None:
48+ if other_affected == 1:
49 return "This bug affects 1 person. Does this bug affect you?"
50- elif self.current_user_affected_status:
51- return "This bug affects you and 1 other person"
52- else:
53- return "This bug affects 1 person, but not you"
54- elif self.other_users_affected_count > 1:
55- if self.current_user_affected_status is None:
56+ elif other_affected > 1:
57 return (
58 "This bug affects %d people. Does this bug "
59- "affect you?" % (self.other_users_affected_count))
60- elif self.current_user_affected_status:
61- return "This bug affects you and %d other people" % (
62- self.other_users_affected_count)
63+ "affect you?" % (other_affected))
64 else:
65- return "This bug affects %d people, but not you" % (
66- self.other_users_affected_count)
67- else:
68- if self.current_user_affected_status is None:
69 return "Does this bug affect you?"
70- elif self.current_user_affected_status:
71+ elif me_affected is True:
72+ if other_affected == 0:
73 return "This bug affects you"
74+ elif other_affected == 1:
75+ return "This bug affects you and 1 other person"
76 else:
77+ return "This bug affects you and %d other people" % (
78+ other_affected)
79+ else:
80+ if other_affected == 0:
81 return "This bug doesn't affect you"
82+ elif other_affected == 1:
83+ return "This bug affects 1 person, but not you"
84+ elif other_affected > 1:
85+ return "This bug affects %d people, but not you" % (
86+ other_affected)
87
88- @property
89+ @cachedproperty
90 def anon_affected_statement(self):
91 """The "this bug affects" statement to show to anonymous users.
92
93 The outputs of this method should be mirrored in
94 MeTooChoiceSource._getSourceNames() (Javascript).
95 """
96- if self.context.users_affected_count == 1:
97+ affected = self.total_users_affected_count
98+ if affected == 1:
99 return "This bug affects 1 person"
100- elif self.context.users_affected_count > 1:
101- return "This bug affects %d people" % (
102- self.context.users_affected_count)
103+ elif affected > 1:
104+ return "This bug affects %d people" % affected
105 else:
106 return None
107
108
109=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
110--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-11 18:36:39 +0000
111+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-11-14 05:10:29 +0000
112@@ -122,7 +122,7 @@
113 self.getUserBrowser(url, person_no_teams)
114 # This may seem large: it is; there is easily another 30% fat in
115 # there.
116- self.assertThat(recorder, HasQueryCount(LessThan(84)))
117+ self.assertThat(recorder, HasQueryCount(LessThan(85)))
118 count_with_no_teams = recorder.count
119 # count with many teams
120 self.invalidate_caches(task)
121@@ -138,7 +138,7 @@
122 def test_rendered_query_counts_constant_with_attachments(self):
123 with celebrity_logged_in('admin'):
124 browses_under_limit = BrowsesWithQueryLimit(
125- 86, self.factory.makePerson())
126+ 88, self.factory.makePerson())
127
128 # First test with a single attachment.
129 task = self.factory.makeBugTask()
130@@ -192,7 +192,7 @@
131 # Ideally this should be much fewer, but this tries to keep a win of
132 # removing more than half of these.
133 self.assertThat(recorder, HasQueryCount(
134- LessThan(count_with_no_branches + 45),
135+ LessThan(count_with_no_branches + 46),
136 ))
137
138 def test_interesting_activity(self):
139@@ -335,19 +335,122 @@
140 1, self.view.other_users_affected_count)
141 other_user_1 = self.factory.makePerson()
142 self.bug.markUserAffected(other_user_1, True)
143+ self.refresh()
144 self.failUnlessEqual(
145 2, self.view.other_users_affected_count)
146 other_user_2 = self.factory.makePerson()
147 self.bug.markUserAffected(other_user_2, True)
148+ self.refresh()
149 self.failUnlessEqual(
150 3, self.view.other_users_affected_count)
151 self.bug.markUserAffected(other_user_1, False)
152- self.failUnlessEqual(
153- 2, self.view.other_users_affected_count)
154- self.bug.markUserAffected(self.view.user, True)
155- self.refresh()
156- self.failUnlessEqual(
157- 2, self.view.other_users_affected_count)
158+ self.refresh()
159+ self.failUnlessEqual(
160+ 2, self.view.other_users_affected_count)
161+ self.bug.markUserAffected(self.view.user, True)
162+ self.refresh()
163+ self.failUnlessEqual(
164+ 2, self.view.other_users_affected_count)
165+
166+ def makeDuplicate(self):
167+ user2 = self.factory.makePerson()
168+ self.bug2 = self.factory.makeBug()
169+ self.bug2.markUserAffected(user2, True)
170+ self.assertEqual(
171+ 2, self.bug2.users_affected_count)
172+ self.bug2.markAsDuplicate(self.bug)
173+ # After this there are three users already affected: the creators of
174+ # the two bugs, plus user2. The current user is not yet affected by
175+ # any of them.
176+
177+ def test_counts_user_unaffected(self):
178+ self.useFixture(FeatureFixture(
179+ {'bugs.affected_count_includes_dupes.disabled': ''}))
180+ self.makeDuplicate()
181+ self.assertEqual(
182+ 3, self.view.total_users_affected_count)
183+ self.assertEqual(
184+ "This bug affects 3 people. Does this bug affect you?",
185+ self.view.affected_statement)
186+ self.assertEqual(
187+ "This bug affects 3 people",
188+ self.view.anon_affected_statement)
189+ self.assertEqual(
190+ self.view.other_users_affected_count,
191+ 3)
192+
193+ def test_counts_affected_by_duplicate(self):
194+ self.useFixture(FeatureFixture(
195+ {'bugs.affected_count_includes_dupes.disabled': ''}))
196+ self.makeDuplicate()
197+ # Now with you affected by the duplicate, but not the master.
198+ self.bug2.markUserAffected(self.view.user, True)
199+ self.refresh()
200+ self.assertEqual(
201+ "This bug affects 3 people. Does this bug affect you?",
202+ self.view.affected_statement)
203+ self.assertEqual(
204+ "This bug affects 4 people",
205+ self.view.anon_affected_statement)
206+ self.assertEqual(
207+ self.view.other_users_affected_count,
208+ 3)
209+
210+ def test_counts_affected_by_master(self):
211+ self.useFixture(FeatureFixture(
212+ {'bugs.affected_count_includes_dupes.disabled': ''}))
213+ self.makeDuplicate()
214+ # And now with you also affected by the master.
215+ self.bug.markUserAffected(self.view.user, True)
216+ self.refresh()
217+ self.assertEqual(
218+ "This bug affects you and 3 other people",
219+ self.view.affected_statement)
220+ self.assertEqual(
221+ "This bug affects 4 people",
222+ self.view.anon_affected_statement)
223+ self.assertEqual(
224+ self.view.other_users_affected_count,
225+ 3)
226+
227+ def test_counts_affected_by_duplicate_not_by_master(self):
228+ self.useFixture(FeatureFixture(
229+ {'bugs.affected_count_includes_dupes.disabled': ''}))
230+ self.makeDuplicate()
231+ self.bug2.markUserAffected(self.view.user, True)
232+ self.bug.markUserAffected(self.view.user, False)
233+ # You're not included in this count, even though you are affected by
234+ # the dupe.
235+ self.assertEqual(
236+ "This bug affects 3 people, but not you",
237+ self.view.affected_statement)
238+ # It would be reasonable for Anon to see this bug cluster affecting
239+ # either 3 or 4 people. However at the moment the "No" answer on the
240+ # master is more authoritative than the "Yes" on the dupe.
241+ self.assertEqual(
242+ "This bug affects 3 people",
243+ self.view.anon_affected_statement)
244+ self.assertEqual(
245+ self.view.other_users_affected_count,
246+ 3)
247+
248+ def test_total_users_affected_count_without_dupes(self):
249+ self.useFixture(FeatureFixture(
250+ {'bugs.affected_count_includes_dupes.disabled': 'on'}))
251+ self.makeDuplicate()
252+ self.refresh()
253+ # Does not count the two users of bug2, so just 1.
254+ self.assertEqual(
255+ 1, self.view.total_users_affected_count)
256+ self.assertEqual(
257+ "This bug affects 1 person. Does this bug affect you?",
258+ self.view.affected_statement)
259+ self.assertEqual(
260+ "This bug affects 1 person",
261+ self.view.anon_affected_statement)
262+ self.assertEqual(
263+ 1,
264+ self.view.other_users_affected_count)
265
266 def test_affected_statement_no_one_affected(self):
267 self.bug.markUserAffected(self.bug.owner, False)
268
269=== modified file 'lib/lp/bugs/configure.zcml'
270--- lib/lp/bugs/configure.zcml 2011-10-27 23:32:17 +0000
271+++ lib/lp/bugs/configure.zcml 2011-11-14 05:10:29 +0000
272@@ -818,6 +818,7 @@
273 users_affected
274 users_unaffected
275 users_affected_count_with_dupes
276+ other_users_affected_count_with_dupes
277 users_affected_with_dupes
278 bug_messages
279 isUserAffected
280
281=== modified file 'lib/lp/bugs/interfaces/bug.py'
282--- lib/lp/bugs/interfaces/bug.py 2011-10-25 02:12:44 +0000
283+++ lib/lp/bugs/interfaces/bug.py 2011-11-14 05:10:29 +0000
284@@ -1,4 +1,4 @@
285-# Copyright 2009 Canonical Ltd. This software is licensed under the
286+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
287 # GNU Affero General Public License version 3 (see the file LICENSE).
288
289 # pylint: disable-msg=E0211,E0213,E0602
290@@ -374,13 +374,17 @@
291 value_type=Reference(schema=IPerson),
292 readonly=True)))
293 users_affected_count_with_dupes = exported(
294- Int(title=_('The number of users affected by this bug '
295- '(including duplicates)'),
296- required=True, readonly=True))
297+ Int(title=_('The number of users affected by this bug '
298+ '(including duplicates)'),
299+ required=True, readonly=True))
300+ other_users_affected_count_with_dupes = exported(
301+ Int(title=_('The number of users affected by this bug '
302+ '(including duplicates), excluding the current user'),
303+ required=True, readonly=True))
304 users_affected_with_dupes = exported(doNotSnapshot(CollectionField(
305- title=_('Users affected (including duplicates)'),
306- value_type=Reference(schema=IPerson),
307- readonly=True)))
308+ title=_('Users affected (including duplicates)'),
309+ value_type=Reference(schema=IPerson),
310+ readonly=True)))
311
312 heat = exported(
313 Int(title=_("The 'heat' of the bug"),
314
315=== modified file 'lib/lp/bugs/model/bug.py'
316--- lib/lp/bugs/model/bug.py 2011-11-07 21:25:46 +0000
317+++ lib/lp/bugs/model/bug.py 2011-11-14 05:10:29 +0000
318@@ -481,6 +481,15 @@
319 return self.users_affected_with_dupes.count()
320
321 @property
322+ def other_users_affected_count_with_dupes(self):
323+ """See `IBug`."""
324+ current_user = getUtility(ILaunchBag).user
325+ if not current_user:
326+ return self.users_affected_count_with_dupes
327+ return self.users_affected_with_dupes.find(
328+ Person.id != current_user.id).count()
329+
330+ @property
331 def indexed_messages(self):
332 """See `IMessageTarget`."""
333 # Note that this is a decorated result set, so will cache its
334
335=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
336--- lib/lp/bugs/stories/webservice/xx-bug.txt 2011-09-30 16:05:20 +0000
337+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2011-11-14 05:10:29 +0000
338@@ -39,6 +39,7 @@
339 messages_collection_link: u'http://.../bugs/11/messages'
340 name: None
341 number_of_duplicates: 0
342+ other_users_affected_count_with_dupes: 0
343 owner_link: u'http://.../~name16'
344 private: False
345 resource_type_link: u'http://.../#bug'
346
347=== modified file 'lib/lp/services/features/flags.py'
348--- lib/lp/services/features/flags.py 2011-11-03 13:58:07 +0000
349+++ lib/lp/services/features/flags.py 2011-11-14 05:10:29 +0000
350@@ -47,6 +47,12 @@
351 '',
352 '',
353 ''),
354+ ('bugs.affected_count_includes_dupes.disabled',
355+ 'boolean',
356+ ("Disable adding up affected users across all duplicate bugs."),
357+ '',
358+ '',
359+ 'https://bugs.launchpad.net/launchpad/+bug/678090'),
360 ('bugs.bugtracker_components.enabled',
361 'boolean',
362 ('Enables the display of bugtracker components.'),