Merge lp:~adeuring/launchpad/bug-sorting into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Abel Deuring
Approved revision: no longer in the source branch.
Merged at revision: 14297
Proposed branch: lp:~adeuring/launchpad/bug-sorting
Merge into: lp:launchpad
Diff against target: 236 lines (+119/-2)
2 files modified
lib/lp/bugs/model/bugtask.py (+30/-2)
lib/lp/bugs/tests/test_bugtask_search.py (+89/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-sorting
Reviewer Review Type Date Requested Status
Deryck Hodge (community) code Approve
Review via email: mp+82217@code.launchpad.net

Commit message

[r=deryck][no-qa] bugtask sorting by milestone name

Description of the change

This branch allows to sort bug searches results by milestone name.
(part of https://dev.launchpad.net/LEP/CustomBugListings)

This is basically a boilerplate addition of another entry to
the dictionary BugTaskSet._ORDERBY_COLUMN, which maps query strings
specifying the order to table columns.

Since the milestone table is normally not included in the FROM clause
of an SQL query, I added slightly more complex data to this dictionary:
Aside from the sort expression, the entry for "milestone_name" contains
also the class Milestone itself and a LeftJoin. This data is used to
include the join in the list join_tables, which in turn used to build
the FROM clause.

I also explicitly added a "second order" sorting, since milestones are
not the most fine grained sort option: By default, the "second level
sort order" would be bug.id or bugtask.id (depending on the search target),
and this is in many use cases probably not very helpful.

test:

./bin/test bugs -vvt test_bugtask_search.*test_sort_by_milestone_name

no lint

To post a comment you must log in.
Revision history for this message
Deryck Hodge (deryck) wrote :

Looks good.

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/model/bugtask.py'
2--- lib/lp/bugs/model/bugtask.py 2011-11-01 05:30:11 +0000
3+++ lib/lp/bugs/model/bugtask.py 2011-11-14 21:39:30 +0000
4@@ -2298,7 +2298,8 @@
5 "BugTask.datecreated > %s" % (
6 sqlvalues(params.created_since,)))
7
8- orderby_arg = self._processOrderBy(params)
9+ orderby_arg, extra_joins = self._processOrderBy(params)
10+ join_tables.extend(extra_joins)
11
12 query = " AND ".join(extra_clauses)
13
14@@ -3160,6 +3161,8 @@
15
16 def getOrderByColumnDBName(self, col_name):
17 """See `IBugTaskSet`."""
18+ # Avoid circular imports.
19+ from lp.registry.model.milestone import Milestone
20 if BugTaskSet._ORDERBY_COLUMN is None:
21 # Local import of Bug to avoid import loop.
22 from lp.bugs.model.bug import Bug
23@@ -3182,6 +3185,10 @@
24 "users_affected_count": Bug.users_affected_count,
25 "heat": BugTask.heat,
26 "latest_patch_uploaded": Bug.latest_patch_uploaded,
27+ "milestone_name": (
28+ Milestone.name,
29+ (Milestone,
30+ LeftJoin(Milestone, BugTask.milestone == Milestone.id))),
31 }
32 return BugTaskSet._ORDERBY_COLUMN[col_name]
33
34@@ -3230,16 +3237,37 @@
35
36 # Translate orderby keys into corresponding Table.attribute
37 # strings.
38+ extra_joins = []
39 ambiguous = True
40+ # Sorting by milestone only is a very "coarse" sort order.
41+ # If no additional sort order is specified, add the bug task
42+ # importance as a secondary sort order.
43+ if len(orderby) == 1:
44+ if orderby[0] == 'milestone_name':
45+ # We want the most important bugtasks first; these have
46+ # larger integer values.
47+ orderby.append('-importance')
48+ elif orderby[0] == '-milestone_name':
49+ orderby.append('importance')
50+ else:
51+ # Other sort orders don't need tweaking.
52+ pass
53+
54 for orderby_col in orderby:
55 if isinstance(orderby_col, SQLConstant):
56 orderby_arg.append(orderby_col)
57 continue
58 if orderby_col.startswith("-"):
59 col = self.getOrderByColumnDBName(orderby_col[1:])
60+ if isinstance(col, tuple):
61+ extra_joins.append(col[1])
62+ col = col[0]
63 order_clause = Desc(col)
64 else:
65 col = self.getOrderByColumnDBName(orderby_col)
66+ if isinstance(col, tuple):
67+ extra_joins.append(col[1])
68+ col = col[0]
69 order_clause = col
70 if col in unambiguous_cols:
71 ambiguous = False
72@@ -3251,7 +3279,7 @@
73 else:
74 orderby_arg.append(BugTask.id)
75
76- return tuple(orderby_arg)
77+ return tuple(orderby_arg), extra_joins
78
79 def getBugCountsForPackages(self, user, packages):
80 """See `IBugTaskSet`."""
81
82=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
83--- lib/lp/bugs/tests/test_bugtask_search.py 2011-10-31 02:15:10 +0000
84+++ lib/lp/bugs/tests/test_bugtask_search.py 2011-11-14 21:39:30 +0000
85@@ -600,6 +600,16 @@
86 params = self.getBugTaskSearchParams(user=None, has_cve=True)
87 self.assertSearchFinds(params, self.bugtasks[:1])
88
89+ def test_sort_by_milestone_name(self):
90+ expected = self.setUpMilestoneSorting()
91+ params = self.getBugTaskSearchParams(
92+ user=None, orderby='milestone_name')
93+ self.assertSearchFinds(params, expected)
94+ expected.reverse()
95+ params = self.getBugTaskSearchParams(
96+ user=None, orderby='-milestone_name')
97+ self.assertSearchFinds(params, expected)
98+
99
100 class DeactivatedProductBugTaskTestCase(TestCaseWithFactory):
101
102@@ -791,6 +801,16 @@
103 # main bug target.
104 return self._findBugtaskForOtherProduct(bugtask, self.searchtarget)
105
106+ def setUpMilestoneSorting(self):
107+ with person_logged_in(self.owner):
108+ milestone_1 = self.factory.makeMilestone(
109+ product=self.searchtarget, name='1.0')
110+ milestone_2 = self.factory.makeMilestone(
111+ product=self.searchtarget, name='2.0')
112+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
113+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
114+ return self.bugtasks[1:] + self.bugtasks[:1]
115+
116
117 class ProductSeriesTarget(BugTargetTestBase):
118 """Use a product series as the bug target."""
119@@ -838,6 +858,16 @@
120 return self._findBugtaskForOtherProduct(
121 bugtask, self.searchtarget.product)
122
123+ def setUpMilestoneSorting(self):
124+ with person_logged_in(self.owner):
125+ milestone_1 = self.factory.makeMilestone(
126+ productseries=self.searchtarget, name='1.0')
127+ milestone_2 = self.factory.makeMilestone(
128+ productseries=self.searchtarget, name='2.0')
129+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
130+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
131+ return self.bugtasks[1:] + self.bugtasks[:1]
132+
133
134 class ProjectGroupTarget(BugTargetTestBase, BugTargetWithBugSuperVisor,
135 ProjectGroupAndDistributionTests):
136@@ -947,6 +977,16 @@
137 # With a flag set, no bugs are found.
138 self.assertSearchFinds(params, [])
139
140+ def setUpMilestoneSorting(self):
141+ with person_logged_in(self.owner):
142+ milestone_1 = self.factory.makeMilestone(
143+ product=self.bugtasks[1].target, name='1.0')
144+ milestone_2 = self.factory.makeMilestone(
145+ product=self.bugtasks[2].target, name='2.0')
146+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
147+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
148+ return self.bugtasks[1:] + self.bugtasks[:1]
149+
150
151 class MilestoneTarget(BugTargetTestBase):
152 """Use a milestone as the bug target."""
153@@ -992,6 +1032,15 @@
154 # main bug target.
155 return self._findBugtaskForOtherProduct(bugtask, self.product)
156
157+ def setUpMilestoneSorting(self):
158+ # Setup for a somewhat pointless test: All bugtasks are already
159+ # assigned to same milestone. This means essentially that the
160+ # search result should be ordered by the secondary sort order,
161+ # BugTask.importance.
162+ # Note that reversing the sort direction of milestone does not
163+ # affect the sort direction of the bug ID.
164+ return sorted(self.bugtasks, key=lambda bugtask: bugtask.importance)
165+
166
167 class DistributionTarget(BugTargetTestBase, ProductAndDistributionTests,
168 BugTargetWithBugSuperVisor,
169@@ -1032,6 +1081,16 @@
170 subscriber, subscribed_by=subscriber)
171 return subscriber
172
173+ def setUpMilestoneSorting(self):
174+ with person_logged_in(self.owner):
175+ milestone_1 = self.factory.makeMilestone(
176+ distribution=self.searchtarget, name='1.0')
177+ milestone_2 = self.factory.makeMilestone(
178+ distribution=self.searchtarget, name='2.0')
179+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
180+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
181+ return self.bugtasks[1:] + self.bugtasks[:1]
182+
183
184 class DistroseriesTarget(BugTargetTestBase):
185 """Use a distro series as the bug target."""
186@@ -1055,6 +1114,16 @@
187 def setBugParamsTarget(self, params, target):
188 params.setDistroSeries(target)
189
190+ def setUpMilestoneSorting(self):
191+ with person_logged_in(self.owner):
192+ milestone_1 = self.factory.makeMilestone(
193+ distribution=self.searchtarget.distribution, name='1.0')
194+ milestone_2 = self.factory.makeMilestone(
195+ distribution=self.searchtarget.distribution, name='2.0')
196+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
197+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
198+ return self.bugtasks[1:] + self.bugtasks[:1]
199+
200
201 class SourcePackageTarget(BugTargetTestBase):
202 """Use a source package as the bug target."""
203@@ -1091,6 +1160,16 @@
204 def targetToGroup(self, target):
205 return target.sourcepackagename.id
206
207+ def setUpMilestoneSorting(self):
208+ with person_logged_in(self.owner):
209+ milestone_1 = self.factory.makeMilestone(
210+ distribution=self.searchtarget.distribution, name='1.0')
211+ milestone_2 = self.factory.makeMilestone(
212+ distribution=self.searchtarget.distribution, name='2.0')
213+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
214+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
215+ return self.bugtasks[1:] + self.bugtasks[:1]
216+
217
218 class DistributionSourcePackageTarget(BugTargetTestBase,
219 BugTargetWithBugSuperVisor):
220@@ -1126,6 +1205,16 @@
221 def targetToGroup(self, target):
222 return target.sourcepackagename.id
223
224+ def setUpMilestoneSorting(self):
225+ with person_logged_in(self.owner):
226+ milestone_1 = self.factory.makeMilestone(
227+ distribution=self.searchtarget.distribution, name='1.0')
228+ milestone_2 = self.factory.makeMilestone(
229+ distribution=self.searchtarget.distribution, name='2.0')
230+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
231+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
232+ return self.bugtasks[1:] + self.bugtasks[:1]
233+
234
235 bug_targets_mixins = (
236 DistributionTarget,