Merge lp:~wallyworld/launchpad/disabled-project-bugs-shown into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12816
Proposed branch: lp:~wallyworld/launchpad/disabled-project-bugs-shown
Merge into: lp:launchpad
Diff against target: 147 lines (+81/-13)
2 files modified
lib/lp/bugs/browser/bugtask.py (+16/-9)
lib/lp/bugs/browser/tests/test_bugtask.py (+65/-4)
To merge this branch: bzr merge lp:~wallyworld/launchpad/disabled-project-bugs-shown
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
William Grant code* Approve
Review via email: mp+57265@code.launchpad.net

Commit message

[r=lifeless,wgrant][bug=682863] Prevent the bug tasks table from showing tasks for inactive projects and product/distro series.

Description of the change

A simple fix to prevent the bug tasks table from showing tasks for inactive projects and product/distro series.

== Implementation ==

Use a BugTaskSet search to load the bug tasks for the given bug instead of reusing the already cached entire bug task list. This uses a bit extra sql but means less setup work is done to construct data objects which would otherwise not be used.

If you navigate to the inactive project, it is still possible to click through from there to a related bug. But when you get to the bug index page, there will be no bug task entry for the inactive project, which is fine.

== Tests ==

Add to existing bugs/browser/tests/bugtask tests:

TestBugTasksAndNominationsView
  test_bugtask_listing_for_inactive_projects
  test_listing_with_no_bugtasks

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/browser/tests/test_bugtask.py

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Looks good. But could you use bugtask.pillar.active instead of the disjunctions? Also, I don't understand why you set both series' statuses to EXPERIMENTAL.

review: Approve (code*)
Revision history for this message
Robert Collins (lifeless) wrote :

So there are more issues here.

Firstly, code & structure: Much better to write up these changes as unit tests not doctests, and there isn't any need really to add to the doctest.

More importantly, doing a filter in the list comprehension is inefficient and leaves waste in the system: note that the getBugTaskAndNominationViews gets bugtasks from self.bugtasks - and that that set of bugtasks is eager loaded and used for other things such as target calculations and milestone lookups. The filtering should, as always, be done on data retrieval or at minimum at the first poin where we might eager load.

Testing: Corner cases; what if all the tasks on the bug are hidden? That may be 'cannot be traversed to'.

Lastly: Elsewhere in the system disabled things are only hidden for non-admin, non-registry-experts; that seems appropriate to apply here and would resolve the testing case.

Lastly lastly: I suspect that the bugtask search for a given bug would resolve all of these issues at once (vs using a bugtasks attribute on bug).

review: Needs Fixing
Revision history for this message
Ian Booth (wallyworld) wrote :

The reason for the series status -> experimental is that the "active" on
product|distro series is a read only property based on the value of the
status.

I'll look at changing to using bugtask.pillar.active

On 12/04/11 11:52, William Grant wrote:
> Review: Approve code*
> Looks good. But could you use bugtask.pillar.active instead of the disjunctions? Also, I don't understand why you set both series' statuses to EXPERIMENTAL.

Revision history for this message
Ian Booth (wallyworld) wrote :

I've changed the implementation as per the suggestion to use a BugTaskSet search to load the bug tasks. I've also moved the tests from doc tests to unit tests as requested.

Revision history for this message
William Grant (wgrant) wrote :

This looks pretty good. Normal users are unable to see a bug with all its tasks deactivated, but that's because of a long-standing bug in Bug.default_bugtask (it always uses the earliest task, regardless of whether it's visible).

I note that BugTaskSearchParams always excludes inactive projects, regardless of privilege. This means that registry admins will see an empty task table, which probably isn't too bad.

32 + if len(self.bugtasks):
33 + self.milestones = list(
34 + bugtask_set.getBugTaskTargetMilestones(self.bugtasks))

Does it break if self.bugtasks is empty? If not, why the guard?

72 - self.assertThat(recorder, HasQueryCount(LessThan(67)))
73 + self.assertThat(recorder, HasQueryCount(LessThan(69)))

I only see one new query. What's the second?

103 + foo_bug = self.factory.makeBug(product=product_foo)
104 + bugtask_set = getUtility(IBugTaskSet)
105 + bugtask_set.createTask(
106 + bug=foo_bug, owner=foo_bug.owner, product=product_bar)

Consider Bug.addTask or LOF.makeBugTask for the second task.

108 + removeSecurityProxy(product_bar).active = False
132 + removeSecurityProxy(product_foo).active = False

Consider letting LOF.makeProduct take an "active" parameter.

review: Approve (code*)
Revision history for this message
Ian Booth (wallyworld) wrote :

> 32 + if len(self.bugtasks):
> 33 + self.milestones = list(
> 34 + bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
>
> Does it break if self.bugtasks is empty? If not, why the guard?
>

Yes. getBugTaskTargetMilestones() breaks.

> 72 - self.assertThat(recorder, HasQueryCount(LessThan(67)))
> 73 + self.assertThat(recorder, HasQueryCount(LessThan(69)))
>
> I only see one new query. What's the second?
>

From what I saw, the section of code with the extra query is actually
called twice when the page loads. I've not yet tracked down why.

> 103 + foo_bug = self.factory.makeBug(product=product_foo)
> 104 + bugtask_set = getUtility(IBugTaskSet)
> 105 + bugtask_set.createTask(
> 106 + bug=foo_bug, owner=foo_bug.owner, product=product_bar)
>
> Consider Bug.addTask or LOF.makeBugTask for the second task.
>

Ok. I copied from what was in the doctest. Bad move perhaps :-)

> 108 + removeSecurityProxy(product_bar).active = False
> 132 + removeSecurityProxy(product_foo).active = False
>
> Consider letting LOF.makeProduct take an "active" parameter.

Considered it but there's already a lot of parameters there and the
number of times we need to change active to false didn't seem to justify
the additional clutter.

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

self.milestones = list()

is unidiomatic -
self.milestones = []
is the usual way this is written.
It might be clearer to do
if self.bugtasks:
 assign here
else:
 self.milestones = []

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

Oh, and you might want to file a bug about registry + admins having the search results unconditionally filtered. Not sure what we should do there, but it will lead to time wasting at some future date.

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-03-30 11:36:37 +0000
3+++ lib/lp/bugs/browser/bugtask.py 2011-04-13 06:14:46 +0000
4@@ -3119,19 +3119,26 @@
5 def initialize(self):
6 """Cache the list of bugtasks and set up the release mapping."""
7 # Cache some values, so that we don't have to recalculate them
8- # for each bug task. This query is redundant:
9- # the publisher also queries all the bugtasks.
10- self.bugtasks = list(self.context.bugtasks)
11+ # for each bug task.
12+ # Note: even though the publisher queries all the bugtasks and we in
13+ # theory could just reuse that already loaded list here, it's better
14+ # to do another query to only load the bug tasks for active projects
15+ # so we don't incur the cost of setting up data structures for tasks
16+ # we will not be showing in the listing.
17+ bugtask_set = getUtility(IBugTaskSet)
18+ search_params = BugTaskSearchParams(user=self.user, bug=self.context)
19+ self.bugtasks = list(bugtask_set.search(search_params))
20 self.many_bugtasks = len(self.bugtasks) >= 10
21 self.cached_milestone_source = CachedMilestoneSourceFactory()
22 self.user_is_subscribed = self.context.isSubscribed(self.user)
23
24- # Pull all of the related milestones into the storm cache, since
25- # they'll be needed for the vocabulary used in this view.
26- bugtask_set = getUtility(IBugTaskSet)
27- self.milestones = list(
28- bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
29-
30+ # Pull all of the related milestones, if any, into the storm cache,
31+ # since they'll be needed for the vocabulary used in this view.
32+ if self.bugtasks:
33+ self.milestones = list(
34+ bugtask_set.getBugTaskTargetMilestones(self.bugtasks))
35+ else:
36+ self.milestones = []
37 distro_packages = defaultdict(list)
38 distro_series_packages = defaultdict(list)
39 for bugtask in self.bugtasks:
40
41=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
42--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-04-05 06:13:39 +0000
43+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-04-13 06:14:46 +0000
44@@ -8,7 +8,10 @@
45 from pytz import UTC
46 from storm.store import Store
47 from testtools.matchers import LessThan
48-from zope.component import getUtility
49+from zope.component import (
50+ getMultiAdapter,
51+ getUtility,
52+ )
53 from zope.security.proxy import removeSecurityProxy
54
55 from canonical.launchpad.ftests import (
56@@ -29,7 +32,12 @@
57 BugTasksAndNominationsView,
58 )
59 from lp.bugs.interfaces.bugactivity import IBugActivitySet
60-from lp.bugs.interfaces.bugtask import BugTaskStatus
61+from lp.bugs.interfaces.bugnomination import IBugNomination
62+from lp.bugs.interfaces.bugtask import (
63+ BugTaskStatus,
64+ IBugTask,
65+ IBugTaskSet,
66+ )
67 from lp.services.propertycache import get_property_cache
68 from lp.soyuz.interfaces.component import IComponentSet
69 from lp.testing import (
70@@ -78,7 +86,7 @@
71 self.getUserBrowser(url, person_no_teams)
72 # This may seem large: it is; there is easily another 30% fat in
73 # there.
74- self.assertThat(recorder, HasQueryCount(LessThan(67)))
75+ self.assertThat(recorder, HasQueryCount(LessThan(69)))
76 count_with_no_teams = recorder.count
77 # count with many teams
78 self.invalidate_caches(task)
79@@ -94,7 +102,7 @@
80 def test_rendered_query_counts_constant_with_attachments(self):
81 with celebrity_logged_in('admin'):
82 browses_under_limit = BrowsesWithQueryLimit(
83- 71, self.factory.makePerson())
84+ 73, self.factory.makePerson())
85
86 # First test with a single attachment.
87 task = self.factory.makeBugTask()
88@@ -433,6 +441,59 @@
89 '2008-07-18 10:20:30+00:00 by Tim (tim), maintained by Jim (jim)',
90 self.view.getTargetLinkTitle(bug_task.target))
91
92+ def _get_object_type(self, task_or_nomination):
93+ if IBugTask.providedBy(task_or_nomination):
94+ return "bugtask"
95+ elif IBugNomination.providedBy(task_or_nomination):
96+ return "nomination"
97+ else:
98+ return "unknown"
99+
100+ def test_bugtask_listing_for_inactive_projects(self):
101+ # Bugtasks should only be listed for active projects.
102+
103+ product_foo = self.factory.makeProduct(name="foo")
104+ product_bar = self.factory.makeProduct(name="bar")
105+ foo_bug = self.factory.makeBug(product=product_foo)
106+ bugtask_set = getUtility(IBugTaskSet)
107+ bugtask_set.createTask(
108+ bug=foo_bug, owner=foo_bug.owner, product=product_bar)
109+
110+ removeSecurityProxy(product_bar).active = False
111+
112+ request = LaunchpadTestRequest()
113+ foo_bugtasks_and_nominations_view = getMultiAdapter(
114+ (foo_bug, request), name="+bugtasks-and-nominations-table")
115+ foo_bugtasks_and_nominations_view.initialize()
116+
117+ task_and_nomination_views = (
118+ foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
119+ actual_results = []
120+ for task_or_nomination_view in task_and_nomination_views:
121+ task_or_nomination = task_or_nomination_view.context
122+ actual_results.append((
123+ self._get_object_type(task_or_nomination),
124+ task_or_nomination.status.title,
125+ task_or_nomination.target.bugtargetdisplayname))
126+ # Only the one active project's task should be listed.
127+ self.assertEqual([("bugtask", "New", "Foo")], actual_results)
128+
129+ def test_listing_with_no_bugtasks(self):
130+ # Test the situation when there are no bugtasks to show.
131+
132+ product_foo = self.factory.makeProduct(name="foo")
133+ foo_bug = self.factory.makeBug(product=product_foo)
134+ removeSecurityProxy(product_foo).active = False
135+
136+ request = LaunchpadTestRequest()
137+ foo_bugtasks_and_nominations_view = getMultiAdapter(
138+ (foo_bug, request), name="+bugtasks-and-nominations-table")
139+ foo_bugtasks_and_nominations_view.initialize()
140+
141+ task_and_nomination_views = (
142+ foo_bugtasks_and_nominations_view.getBugTaskAndNominationViews())
143+ self.assertEqual([], task_and_nomination_views)
144+
145
146 class TestBugTaskEditViewStatusField(TestCaseWithFactory):
147 """We show only those options as possible value in the status