Merge lp:~deryck/launchpad/max-heat-by-target-511382 into lp:launchpad/db-devel
- max-heat-by-target-511382
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Eleanor Berger |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~deryck/launchpad/max-heat-by-target-511382 |
Merge into: | lp:launchpad/db-devel |
Diff against target: |
650 lines (+241/-52) 19 files modified
database/schema/comments.sql (+4/-2) database/schema/patch-2207-34-0.sql (+11/-0) lib/lp/bugs/browser/bug.py (+0/-22) lib/lp/bugs/browser/bugtask.py (+33/-8) lib/lp/bugs/browser/configure.zcml (+0/-5) lib/lp/bugs/browser/tests/bug-heat-view.txt (+44/-11) lib/lp/bugs/interfaces/bug.py (+1/-0) lib/lp/bugs/interfaces/bugtarget.py (+5/-0) lib/lp/bugs/model/bug.py (+1/-0) lib/lp/bugs/model/bugtarget.py (+14/-0) lib/lp/bugs/tests/test_bugheat.py (+82/-0) lib/lp/registry/configure.zcml (+2/-1) lib/lp/registry/model/distribution.py (+2/-2) lib/lp/registry/model/distributionsourcepackage.py (+23/-0) lib/lp/registry/model/distroseries.py (+5/-0) lib/lp/registry/model/product.py (+2/-1) lib/lp/registry/model/productseries.py (+5/-0) lib/lp/registry/model/project.py (+2/-0) lib/lp/registry/model/sourcepackage.py (+5/-0) |
To merge this branch: | bzr merge lp:~deryck/launchpad/max-heat-by-target-511382 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Björn Tillenius (community) | db | Approve | |
Eleanor Berger (community) | Approve | ||
Stuart Bishop (community) | db | Approve | |
Review via email: mp+19998@code.launchpad.net |
Commit message
Add a max_heat attribute to each bug target.
Description of the change
Deryck Hodge (deryck) wrote : | # |
Stuart Bishop (stub) wrote : | # |
Will Launchpad have other heat in the future? If so, the column needs to be called max_bug_heat to avoid future name conflicts.
Is 0 a sane default? For heat calculations, I imagine this could lead to division by zero errors. If there is no sane default and the heat calculations explicitly checks for '0', we should just use NULL instead.
Otherwise fine. Allocated number is patch-2207-
Rather than write a new script to update the max heat cache, I think it would be better to add a new task to garbo-daily.py. This provides infrastructure to do the updates in a database friendly manner (with DBLoopTuner), and test infrastructure to speed up writing your tests.
Björn Tillenius (bjornt) wrote : | # |
I have the same questions as Stuart. For now I think it would be better to call it max_bug_heat to avoid confusion. If we add heat to other things, we can rename it, if we want the heat to be shared.
I also think that NULL should be used to define that heat hasn't been calculated yet, unless using 0 makes things easier. Does it? (Using 0 to have a special value is a bit of a hack, when you are restricted to integer values only.)
I have another question as well, though. You have max_heat for Distribution, DistroSeries, and DistributionSou
Deryck Hodge (deryck) wrote : | # |
I'll update the patch and code to use max_bug_heat, that makes sense. I'm also fine to use NULL; again that makes sense. Thanks for the reviews!
I'll fix the branch now and follow up with Björn on IRC about the DistroSeriesSou
Cheers,
deryck
Deryck Hodge (deryck) wrote : | # |
Okay, I've updated based on the recommendations here.
I talked to Björn and after a long discussion on IRC came to the conclusion to have max_bug_heat on Product, Project, Distribution, and DistributionSou
Does this look good to you guys?
Cheers,
deryck
Stuart Bishop (stub) wrote : | # |
db patch looks good.
Eleanor Berger (intellectronica) wrote : | # |
As we discussed on IRC, it will be safer to avoid having a setter for DSP.max_bug_heat, since it uses the distro's value. With that change, r=me.
Björn Tillenius (bjornt) : | # |
Preview Diff
1 | === modified file 'database/schema/comments.sql' |
2 | --- database/schema/comments.sql 2010-02-23 15:43:49 +0000 |
3 | +++ database/schema/comments.sql 2010-02-26 05:16:22 +0000 |
4 | @@ -461,7 +461,7 @@ |
5 | |
6 | COMMENT ON TABLE DistributionSourcePackage IS 'Representing a sourcepackage in a distribution across all distribution series.'; |
7 | COMMENT ON COLUMN DistributionSourcePackage.bug_reporting_guidelines IS 'Guidelines to the end user for reporting bugs on a particular a source package in a distribution.'; |
8 | - |
9 | +COMMENT ON COLUMN DistributionSourcePackage.max_bug_heat IS 'The highest heat value across bugs for this source package.'; |
10 | |
11 | -- DistributionSourcePackageCache |
12 | |
13 | @@ -475,7 +475,6 @@ |
14 | COMMENT ON COLUMN DistributionSourcePackageCache.changelog IS 'A concatenation of the source package release changelogs for this source package, where the status is not REMOVED.'; |
15 | COMMENT ON COLUMN DistributionSourcePackageCache.archive IS 'The archive where the source is published.'; |
16 | |
17 | - |
18 | -- DistroSeriesPackageCache |
19 | |
20 | COMMENT ON TABLE DistroSeriesPackageCache IS 'A cache of the text associated with binary packages in the distroseries. This table allows for fast queries to find a binary packagename that matches a given text.'; |
21 | @@ -612,6 +611,7 @@ |
22 | COMMENT ON COLUMN Product.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.'; |
23 | COMMENT ON COLUMN Product.license_approved IS 'The Other/Open Source license has been approved by an administrator.'; |
24 | COMMENT ON COLUMN Product.remote_product IS 'The ID of this product on its remote bug tracker.'; |
25 | +COMMENT ON COLUMN Product.max_bug_heat IS 'The highest heat value across bugs for this product.'; |
26 | |
27 | -- ProductLicense |
28 | COMMENT ON TABLE ProductLicense IS 'The licenses that cover the software for a product.'; |
29 | @@ -703,6 +703,7 @@ |
30 | COMMENT ON COLUMN Project.logo IS 'The library file alias of a smaller version of this product''s mugshot.'; |
31 | COMMENT ON COLUMN Project.bug_reporting_guidelines IS 'Guidelines to the end user for reporting bugs on products in this project.'; |
32 | COMMENT ON COLUMN Project.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.'; |
33 | +COMMENT ON COLUMN Project.max_bug_heat IS 'The highest heat value across bugs for products in this project.'; |
34 | |
35 | -- ProjectRelationship |
36 | COMMENT ON TABLE ProjectRelationship IS 'Project Relationships. This table stores information about the way projects are related to one another in the open source world. The actual nature of the relationship is stored in the ''label'' field, and possible values are given by the ProjectRelationship enum in dbschema.py. Examples are AGGREGATES ("the Gnome Project AGGREGATES EOG and Evolution and Gnumeric and AbiWord") and SIMILAR ("the Evolution project is SIMILAR to the Mutt project").'; |
37 | @@ -966,6 +967,7 @@ |
38 | COMMENT ON COLUMN Distribution.enable_bug_expiration IS 'Indicates whether automatic bug expiration is enabled.'; |
39 | COMMENT ON COLUMN Distribution.bug_reporting_guidelines IS 'Guidelines to the end user for reporting bugs on this distribution.'; |
40 | COMMENT ON COLUMN Distribution.reviewer_whiteboard IS 'A whiteboard for Launchpad admins, registry experts and the project owners to capture the state of current issues with the project.'; |
41 | +COMMENT ON COLUMN Distribution.max_bug_heat IS 'The highest heat value across bugs for this distribution.'; |
42 | |
43 | -- DistroSeries |
44 | |
45 | |
46 | === added file 'database/schema/patch-2207-34-0.sql' |
47 | --- database/schema/patch-2207-34-0.sql 1970-01-01 00:00:00 +0000 |
48 | +++ database/schema/patch-2207-34-0.sql 2010-02-26 05:16:22 +0000 |
49 | @@ -0,0 +1,11 @@ |
50 | +-- Copyright 2010 Canonical Ltd. This software is licensed under the |
51 | +-- GNU Affero General Public License version 3 (see the file LICENSE). |
52 | + |
53 | +SET client_min_messages=ERROR; |
54 | + |
55 | +ALTER TABLE product ADD COLUMN max_bug_heat integer; |
56 | +ALTER TABLE project ADD COLUMN max_bug_heat integer; |
57 | +ALTER TABLE distribution ADD COLUMN max_bug_heat integer; |
58 | +ALTER TABLE distributionsourcepackage ADD COLUMN max_bug_heat integer; |
59 | + |
60 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 34, 0); |
61 | |
62 | === modified file 'lib/lp/bugs/browser/bug.py' |
63 | --- lib/lp/bugs/browser/bug.py 2010-02-02 17:26:53 +0000 |
64 | +++ lib/lp/bugs/browser/bug.py 2010-02-26 05:16:22 +0000 |
65 | @@ -10,7 +10,6 @@ |
66 | 'BugContextMenu', |
67 | 'BugEditView', |
68 | 'BugFacets', |
69 | - 'BugHeatView', |
70 | 'BugMarkAsAffectingUserView', |
71 | 'BugMarkAsDuplicateView', |
72 | 'BugNavigation', |
73 | @@ -23,13 +22,11 @@ |
74 | 'BugWithoutContextView', |
75 | 'DeprecatedAssignedBugsView', |
76 | 'MaloneView', |
77 | - 'MAX_HEAT', |
78 | ] |
79 | |
80 | from datetime import datetime, timedelta |
81 | from email.MIMEMultipart import MIMEMultipart |
82 | from email.MIMEText import MIMEText |
83 | -from math import floor |
84 | import re |
85 | |
86 | import pytz |
87 | @@ -77,10 +74,6 @@ |
88 | from canonical.widgets.bug import BugTagsWidget |
89 | from canonical.widgets.project import ProjectScopeWidget |
90 | |
91 | -# Constant for the maximum bug heat we'll use for converting |
92 | -# IBug.heat to ratio. In the future this should come from the DB. |
93 | -# The value must be a float |
94 | -MAX_HEAT = 5000.0 |
95 | |
96 | class BugNavigation(Navigation): |
97 | """Navigation for the `IBug`.""" |
98 | @@ -969,18 +962,3 @@ |
99 | return html.encode('utf-8') |
100 | return renderer |
101 | |
102 | - |
103 | -class BugHeatView(LaunchpadView): |
104 | - """View for rendering the graphical (HTML) representation of bug heat.""" |
105 | - |
106 | - def __call__(self): |
107 | - """Render the bug heat representation.""" |
108 | - heat_ratio = floor((self.context.heat / MAX_HEAT) * 4) |
109 | - html = '<span>' |
110 | - for flame in range(1, 5): |
111 | - if flame <= heat_ratio: |
112 | - html += '<img src="/@@/flame-icon" />' |
113 | - else: |
114 | - html += '<img src="/@@/flame-bw-icon" />' |
115 | - html += '</span>' |
116 | - return html |
117 | |
118 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
119 | --- lib/lp/bugs/browser/bugtask.py 2010-02-25 22:00:26 +0000 |
120 | +++ lib/lp/bugs/browser/bugtask.py 2010-02-26 05:16:22 +0000 |
121 | @@ -33,7 +33,9 @@ |
122 | 'BugTaskTextView', |
123 | 'BugTaskView', |
124 | 'BugTasksAndNominationsView', |
125 | + 'bugtask_heat_html', |
126 | 'BugsBugTaskSearchListingView', |
127 | + 'calculate_heat_display', |
128 | 'NominationsReviewTableBatchNavigatorView', |
129 | 'TextualBugTaskSearchListingView', |
130 | 'get_buglisting_search_filter_url', |
131 | @@ -49,6 +51,7 @@ |
132 | from simplejson import dumps |
133 | import urllib |
134 | from operator import attrgetter, itemgetter |
135 | +from math import floor, log |
136 | |
137 | from zope import component |
138 | from zope.app.form import CustomWidgetFactory |
139 | @@ -1080,10 +1083,35 @@ |
140 | @property |
141 | def bug_heat_html(self): |
142 | """HTML representation of the bug heat.""" |
143 | - view = getMultiAdapter( |
144 | - (self.context.bug, self.request), |
145 | - name='+bug-heat') |
146 | - return view() |
147 | + return bugtask_heat_html(self.context) |
148 | + |
149 | + |
150 | +def calculate_heat_display(heat, max_bug_heat): |
151 | + """Calculate the number of heat 'flames' to display.""" |
152 | + heat = float(heat) |
153 | + max_bug_heat = float(max_bug_heat) |
154 | + if heat / max_bug_heat < 0.33333: |
155 | + return 0 |
156 | + if heat / max_bug_heat < 0.66666: |
157 | + return int(floor((heat / max_bug_heat) * 4)) |
158 | + else: |
159 | + return int(floor((log(heat) / log(max_bug_heat)) * 4)) |
160 | + |
161 | + |
162 | +def bugtask_heat_html(bugtask): |
163 | + """Render the HTML representing bug heat for a given bugask.""" |
164 | + max_bug_heat = bugtask.target.max_bug_heat |
165 | + if max_bug_heat is None: |
166 | + max_bug_heat = 5000 |
167 | + heat_ratio = calculate_heat_display(bugtask.bug.heat, max_bug_heat) |
168 | + html = '<span>' |
169 | + for flame in range(1, 5): |
170 | + if flame <= heat_ratio: |
171 | + html += '<img src="/@@/flame-icon" />' |
172 | + else: |
173 | + html += '<img src="/@@/flame-bw-icon" />' |
174 | + html += '</span>' |
175 | + return html |
176 | |
177 | |
178 | class BugTaskPortletView: |
179 | @@ -1967,10 +1995,7 @@ |
180 | @property |
181 | def bug_heat_html(self): |
182 | """Returns the bug heat flames HTML.""" |
183 | - view = getMultiAdapter( |
184 | - (self.bugtask.bug, self.request), |
185 | - name='+bug-heat') |
186 | - return view() |
187 | + return bugtask_heat_html(self.bugtask) |
188 | |
189 | |
190 | class BugListingBatchNavigator(TableBatchNavigator): |
191 | |
192 | === modified file 'lib/lp/bugs/browser/configure.zcml' |
193 | --- lib/lp/bugs/browser/configure.zcml 2010-02-20 13:16:38 +0000 |
194 | +++ lib/lp/bugs/browser/configure.zcml 2010-02-26 05:16:22 +0000 |
195 | @@ -1044,11 +1044,6 @@ |
196 | name="+bug-portlet-subscribers-ids" |
197 | class="lp.bugs.browser.bugsubscription.BugPortletSubcribersIds" |
198 | permission="zope.Public"/> |
199 | - <browser:page |
200 | - for="lp.bugs.interfaces.bug.IBug" |
201 | - class="lp.bugs.browser.bug.BugHeatView" |
202 | - permission="zope.Public" |
203 | - name="+bug-heat"/> |
204 | <browser:navigation |
205 | module="lp.bugs.browser.bug" |
206 | classes=" |
207 | |
208 | === modified file 'lib/lp/bugs/browser/tests/bug-heat-view.txt' |
209 | --- lib/lp/bugs/browser/tests/bug-heat-view.txt 2010-01-18 22:11:00 +0000 |
210 | +++ lib/lp/bugs/browser/tests/bug-heat-view.txt 2010-02-26 05:16:22 +0000 |
211 | @@ -1,15 +1,16 @@ |
212 | = Bug heat view = |
213 | |
214 | Bug heat is represented as four flame icons. The quantity of flames that are |
215 | -coloured is dependent on the value of the heat field. The view BugHeatView |
216 | -is used to render the flames. |
217 | +coloured is dependent on the value of the heat field. The function |
218 | +bugtask_heat_html is used to render the flames. |
219 | |
220 | - >>> from lp.bugs.browser.bug import MAX_HEAT |
221 | + >>> MAX_HEAT = 5000.0 |
222 | >>> from canonical.launchpad.ftests import login, logout |
223 | - >>> from canonical.launchpad.webapp import canonical_url |
224 | >>> from zope.security.proxy import removeSecurityProxy |
225 | >>> from BeautifulSoup import BeautifulSoup |
226 | - >>> def print_flames(html): |
227 | + >>> from lp.bugs.browser.bugtask import bugtask_heat_html |
228 | + >>> def print_flames(bugtask): |
229 | + ... html = bugtask_heat_html(bugtask) |
230 | ... soup = BeautifulSoup(html) |
231 | ... for img in soup.span.contents: |
232 | ... print img['src'] |
233 | @@ -20,9 +21,9 @@ |
234 | a heat of half the maximum will result in a display of two coloured flames |
235 | and two black-and-white flames. |
236 | |
237 | + >>> removeSecurityProxy(bug.default_bugtask.target).max_bug_heat = MAX_HEAT |
238 | >>> removeSecurityProxy(bug).heat = MAX_HEAT / 2 |
239 | - >>> bug_heat_view = create_initialized_view(bug, name='+bug-heat') |
240 | - >>> print_flames(bug_heat_view()) |
241 | + >>> print_flames(bug.default_bugtask) |
242 | /@@/flame-icon |
243 | /@@/flame-icon |
244 | /@@/flame-bw-icon |
245 | @@ -31,8 +32,7 @@ |
246 | A bug with a maximum heat will display all four flames coloured. |
247 | |
248 | >>> removeSecurityProxy(bug).heat = MAX_HEAT |
249 | - >>> bug_heat_view = create_initialized_view(bug, name='+bug-heat') |
250 | - >>> print_flames(bug_heat_view()) |
251 | + >>> print_flames(bug.default_bugtask) |
252 | /@@/flame-icon |
253 | /@@/flame-icon |
254 | /@@/flame-icon |
255 | @@ -41,11 +41,44 @@ |
256 | A heat of less than a quarter of the maximum will display no coloured flames. |
257 | |
258 | >>> removeSecurityProxy(bug).heat = 0.1 * MAX_HEAT |
259 | - >>> bug_heat_view = create_initialized_view(bug, name='+bug-heat') |
260 | - >>> print_flames(bug_heat_view()) |
261 | + >>> print_flames(bug.default_bugtask) |
262 | /@@/flame-bw-icon |
263 | /@@/flame-bw-icon |
264 | /@@/flame-bw-icon |
265 | /@@/flame-bw-icon |
266 | |
267 | >>> logout() |
268 | + |
269 | + |
270 | +== Scaling Bug Heat == |
271 | + |
272 | +To ensure a reasonable proportion of cold and hot bugs, the number used to |
273 | +calculate the number of flames to display is not a straight-forward ratio. |
274 | +Instead, we transform it by forcing low heat bugs to produce no flames and |
275 | +scaling the hottest bugs logarithmically. |
276 | + |
277 | + >>> from lp.bugs.browser.bugtask import calculate_heat_display |
278 | + >>> from math import floor |
279 | + |
280 | +Heat values less than a third of the maximum heat don't produce any flames. |
281 | + |
282 | + >>> print int(floor((300.0 / 1000.0) * 4)) |
283 | + 1 |
284 | + >>> print calculate_heat_display(300.0, 1000.0) |
285 | + 0 |
286 | + |
287 | +Heat values higher than a third of the max but lower than two thirds are treated |
288 | +as a straightforward ratio. |
289 | + |
290 | + >>> print int(floor((500.0 / 1000.0) * 4)) |
291 | + 2 |
292 | + >>> print calculate_heat_display(500.0, 1000.0) |
293 | + 2 |
294 | + |
295 | +Heat values higher than two thirds of the maximum heat are scaled upwards. |
296 | + |
297 | + >>> print int(floor((700.0 / 1000.0) * 4)) |
298 | + 2 |
299 | + >>> print calculate_heat_display(800.0, 1000.0) |
300 | + 3 |
301 | + |
302 | |
303 | === modified file 'lib/lp/bugs/interfaces/bug.py' |
304 | --- lib/lp/bugs/interfaces/bug.py 2010-02-23 15:34:15 +0000 |
305 | +++ lib/lp/bugs/interfaces/bug.py 2010-02-26 05:16:22 +0000 |
306 | @@ -15,6 +15,7 @@ |
307 | 'IBugBecameQuestionEvent', |
308 | 'IBugDelta', |
309 | 'IBugSet', |
310 | + 'IFileBugData', |
311 | 'IFrontPageBugAddForm', |
312 | 'IProjectGroupBugAddForm', |
313 | 'InvalidBugTargetType', |
314 | |
315 | === modified file 'lib/lp/bugs/interfaces/bugtarget.py' |
316 | --- lib/lp/bugs/interfaces/bugtarget.py 2010-02-25 22:00:26 +0000 |
317 | +++ lib/lp/bugs/interfaces/bugtarget.py 2010-02-26 05:16:22 +0000 |
318 | @@ -61,6 +61,8 @@ |
319 | description=_("The list of bug tags defined as official."), |
320 | value_type=Tag(), |
321 | readonly=True)) |
322 | + max_bug_heat = Attribute( |
323 | + "The current highest bug heat value for this target.") |
324 | |
325 | @call_with(search_params=None, user=REQUEST_USER) |
326 | @operation_parameters( |
327 | @@ -219,6 +221,9 @@ |
328 | None, all statuses will be included. |
329 | """ |
330 | |
331 | + def setMaxBugHeat(heat): |
332 | + """Set the max_bug_heat for this context.""" |
333 | + |
334 | |
335 | class IBugTarget(IHasBugs): |
336 | """An entity on which a bug can be reported. |
337 | |
338 | === modified file 'lib/lp/bugs/model/bug.py' |
339 | --- lib/lp/bugs/model/bug.py 2010-02-19 16:39:25 +0000 |
340 | +++ lib/lp/bugs/model/bug.py 2010-02-26 05:16:22 +0000 |
341 | @@ -13,6 +13,7 @@ |
342 | 'BugBecameQuestionEvent', |
343 | 'BugSet', |
344 | 'BugTag', |
345 | + 'FileBugData', |
346 | 'get_bug_tags', |
347 | 'get_bug_tags_open_count', |
348 | ] |
349 | |
350 | === modified file 'lib/lp/bugs/model/bugtarget.py' |
351 | --- lib/lp/bugs/model/bugtarget.py 2010-02-25 22:00:26 +0000 |
352 | +++ lib/lp/bugs/model/bugtarget.py 2010-02-26 05:16:22 +0000 |
353 | @@ -25,7 +25,10 @@ |
354 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
355 | from lp.bugs.interfaces.bugtarget import IOfficialBugTag |
356 | from lp.registry.interfaces.distribution import IDistribution |
357 | +from lp.registry.interfaces.distributionsourcepackage import ( |
358 | + IDistributionSourcePackage) |
359 | from lp.registry.interfaces.product import IProduct |
360 | +from lp.registry.interfaces.projectgroup import IProjectGroup |
361 | from lp.bugs.interfaces.bugtask import ( |
362 | BugTagsSearchCombinator, BugTaskImportance, BugTaskSearchParams, |
363 | BugTaskStatus, RESOLVED_BUGTASK_STATUSES, UNRESOLVED_BUGTASK_STATUSES) |
364 | @@ -156,6 +159,17 @@ |
365 | |
366 | return self.searchTasks(all_tasks_query) |
367 | |
368 | + def setMaxBugHeat(self, heat): |
369 | + """See `IHasBugs`.""" |
370 | + if (IDistribution.providedBy(self) |
371 | + or IProduct.providedBy(self) |
372 | + or IProjectGroup.providedBy(self) |
373 | + or IDistributionSourcePackage.providedBy(self)): |
374 | + # Only objects that don't delegate have a setter. |
375 | + self.max_bug_heat = heat |
376 | + else: |
377 | + raise NotImplementedError |
378 | + |
379 | def getBugCounts(self, user, statuses=None): |
380 | """See `IHasBugs`.""" |
381 | if statuses is None: |
382 | |
383 | === modified file 'lib/lp/bugs/tests/test_bugheat.py' |
384 | --- lib/lp/bugs/tests/test_bugheat.py 2010-01-21 22:59:32 +0000 |
385 | +++ lib/lp/bugs/tests/test_bugheat.py 2010-02-26 05:16:22 +0000 |
386 | @@ -20,6 +20,7 @@ |
387 | from lp.bugs.model.bugheat import CalculateBugHeatJob |
388 | from lp.bugs.scripts.bugheat import BugHeatCalculator |
389 | from lp.testing import TestCaseWithFactory |
390 | +from lp.testing.factory import LaunchpadObjectFactory |
391 | |
392 | |
393 | class CalculateBugHeatJobTestCase(TestCaseWithFactory): |
394 | @@ -214,5 +215,86 @@ |
395 | self.assertEqual(bug_job.bug, new_bug) |
396 | |
397 | |
398 | +class MaxHeatByTargetBase: |
399 | + """Base class for testing a bug target's max_bug_heat attribute.""" |
400 | + |
401 | + layer = LaunchpadZopelessLayer |
402 | + |
403 | + factory = LaunchpadObjectFactory() |
404 | + |
405 | + # The target to test. |
406 | + target = None |
407 | + |
408 | + # Does the target have a set method? |
409 | + delegates_setter = False |
410 | + |
411 | + def test_target_max_bug_heat_default(self): |
412 | + self.assertEqual(self.target.max_bug_heat, None) |
413 | + |
414 | + def test_set_target_max_bug_heat(self): |
415 | + if self.delegates_setter: |
416 | + self.assertRaises( |
417 | + NotImplementedError, self.target.setMaxBugHeat, 1000) |
418 | + else: |
419 | + self.target.setMaxBugHeat(1000) |
420 | + self.assertEqual(self.target.max_bug_heat, 1000) |
421 | + |
422 | + |
423 | +class ProjectMaxHeatByTargetTest(MaxHeatByTargetBase, unittest.TestCase): |
424 | + """Ensure a project has a max_bug_heat value that can be set.""" |
425 | + |
426 | + def setUp(self): |
427 | + self.target = self.factory.makeProduct() |
428 | + |
429 | + |
430 | +class DistributionMaxHeatByTargetTest(MaxHeatByTargetBase, unittest.TestCase): |
431 | + """Ensure a distribution has a max_bug_heat value that can be set.""" |
432 | + |
433 | + def setUp(self): |
434 | + self.target = self.factory.makeDistribution() |
435 | + |
436 | + |
437 | +class DistributionSourcePackageMaxHeatByTargetTest( |
438 | + MaxHeatByTargetBase, unittest.TestCase): |
439 | + """Ensure distro source package has max_bug_heat value that can be set.""" |
440 | + |
441 | + def setUp(self): |
442 | + self.target = self.factory.makeDistributionSourcePackage() |
443 | + |
444 | + |
445 | +class SourcePackageMaxHeatByTargetTest( |
446 | + MaxHeatByTargetBase, unittest.TestCase): |
447 | + """Ensure a source package has a max_bug_heat value that can be set.""" |
448 | + |
449 | + def setUp(self): |
450 | + self.target = self.factory.makeSourcePackage() |
451 | + self.delegates_setter = True |
452 | + |
453 | + |
454 | +class ProductSeriesMaxHeatByTargetTest( |
455 | + MaxHeatByTargetBase, unittest.TestCase): |
456 | + """Ensure a product series has a max_bug_heat value that can be set.""" |
457 | + |
458 | + def setUp(self): |
459 | + self.target = self.factory.makeProductSeries() |
460 | + self.delegates_setter = True |
461 | + |
462 | + |
463 | +class DistroSeriesMaxHeatByTargetTest( |
464 | + MaxHeatByTargetBase, unittest.TestCase): |
465 | + """Ensure a distro series has a max_bug_heat value that can be set.""" |
466 | + |
467 | + def setUp(self): |
468 | + self.target = self.factory.makeDistroSeries() |
469 | + self.delegates_setter = True |
470 | + |
471 | + |
472 | +class ProjectGroupMaxHeatByTargetTest( |
473 | + MaxHeatByTargetBase, unittest.TestCase): |
474 | + """Ensure a project group has a max_bug_heat value that can be set.""" |
475 | + |
476 | + def setUp(self): |
477 | + self.target = self.factory.makeProject() |
478 | + |
479 | def test_suite(): |
480 | return unittest.TestLoader().loadTestsFromName(__name__) |
481 | |
482 | === modified file 'lib/lp/registry/configure.zcml' |
483 | --- lib/lp/registry/configure.zcml 2010-02-18 09:04:51 +0000 |
484 | +++ lib/lp/registry/configure.zcml 2010-02-26 05:16:22 +0000 |
485 | @@ -407,7 +407,8 @@ |
486 | official_bug_tags |
487 | findRelatedArchives |
488 | findRelatedArchivePublications |
489 | - userHasBugSubscriptions"/> |
490 | + userHasBugSubscriptions |
491 | + max_bug_heat"/> |
492 | <require |
493 | permission="launchpad.AnyPerson" |
494 | attributes=" |
495 | |
496 | === modified file 'lib/lp/registry/model/distribution.py' |
497 | --- lib/lp/registry/model/distribution.py 2010-02-16 15:08:03 +0000 |
498 | +++ lib/lp/registry/model/distribution.py 2010-02-26 05:16:22 +0000 |
499 | @@ -13,7 +13,7 @@ |
500 | |
501 | from sqlobject import BoolCol, ForeignKey, SQLObjectNotFound, StringCol |
502 | from sqlobject.sqlbuilder import SQLConstant |
503 | -from storm.locals import Desc, In, Join, SQL |
504 | +from storm.locals import Desc, In, Int, Join, SQL |
505 | from storm.store import Store |
506 | from zope.component import getUtility |
507 | from zope.interface import alsoProvides, implements |
508 | @@ -34,7 +34,6 @@ |
509 | from lp.soyuz.model.binarypackagename import BinaryPackageName |
510 | from lp.soyuz.model.binarypackagerelease import ( |
511 | BinaryPackageRelease) |
512 | -from lp.bugs.interfaces.bugattachment import BugAttachmentType |
513 | from lp.bugs.model.bug import ( |
514 | BugSet, get_bug_tags, get_bug_tags_open_count) |
515 | from lp.bugs.model.bugtarget import ( |
516 | @@ -179,6 +178,7 @@ |
517 | official_blueprints = BoolCol(dbName='official_blueprints', notNull=True, |
518 | default=False) |
519 | active = True # Required by IPillar interface. |
520 | + max_bug_heat = Int() |
521 | |
522 | def __repr__(self): |
523 | return "<%s '%s' (%s)>" % ( |
524 | |
525 | === modified file 'lib/lp/registry/model/distributionsourcepackage.py' |
526 | --- lib/lp/registry/model/distributionsourcepackage.py 2010-01-06 12:03:42 +0000 |
527 | +++ lib/lp/registry/model/distributionsourcepackage.py 2010-02-26 05:16:22 +0000 |
528 | @@ -142,6 +142,26 @@ |
529 | _get_bug_reporting_guidelines, |
530 | _set_bug_reporting_guidelines) |
531 | |
532 | + def _get_max_bug_heat(self): |
533 | + """See `IHasBugs`.""" |
534 | + dsp_in_db = self._self_in_database |
535 | + if dsp_in_db is None: |
536 | + return None |
537 | + else: |
538 | + return dsp_in_db.max_bug_heat |
539 | + |
540 | + def _set_max_bug_heat(self, value): |
541 | + """See `IHasBugs`.""" |
542 | + dsp_in_db = self._self_in_database |
543 | + if dsp_in_db is None: |
544 | + dsp_in_db = DistributionSourcePackageInDatabase() |
545 | + dsp_in_db.sourcepackagename = self.sourcepackagename |
546 | + dsp_in_db.distribution = self.distribution |
547 | + Store.of(self.distribution).add(dsp_in_db) |
548 | + dsp_in_db.max_bug_heat = value |
549 | + |
550 | + max_bug_heat = property(_get_max_bug_heat, _set_max_bug_heat) |
551 | + |
552 | @property |
553 | def latest_overall_publication(self): |
554 | """See `IDistributionSourcePackage`.""" |
555 | @@ -474,3 +494,6 @@ |
556 | sourcepackagename_id, 'SourcePackageName.id') |
557 | |
558 | bug_reporting_guidelines = Unicode() |
559 | + |
560 | + max_bug_heat = Int() |
561 | + |
562 | |
563 | === modified file 'lib/lp/registry/model/distroseries.py' |
564 | --- lib/lp/registry/model/distroseries.py 2010-02-22 20:58:16 +0000 |
565 | +++ lib/lp/registry/model/distroseries.py 2010-02-26 05:16:22 +0000 |
566 | @@ -624,6 +624,11 @@ |
567 | return self.fullseriesname |
568 | |
569 | @property |
570 | + def max_bug_heat(self): |
571 | + """See `IHasBugs`.""" |
572 | + return self.distribution.max_bug_heat |
573 | + |
574 | + @property |
575 | def last_full_language_pack_exported(self): |
576 | return LanguagePack.selectFirstBy( |
577 | distroseries=self, type=LanguagePackType.FULL, |
578 | |
579 | === modified file 'lib/lp/registry/model/product.py' |
580 | --- lib/lp/registry/model/product.py 2010-02-09 01:31:05 +0000 |
581 | +++ lib/lp/registry/model/product.py 2010-02-26 05:16:22 +0000 |
582 | @@ -18,7 +18,7 @@ |
583 | import pytz |
584 | from sqlobject import ( |
585 | BoolCol, ForeignKey, SQLMultipleJoin, SQLObjectNotFound, StringCol) |
586 | -from storm.locals import And, Desc, Join, SQL, Store, Unicode |
587 | +from storm.locals import And, Desc, Int, Join, SQL, Store, Unicode |
588 | from zope.interface import implements |
589 | from zope.component import getUtility |
590 | from zope.security.proxy import removeSecurityProxy |
591 | @@ -249,6 +249,7 @@ |
592 | dbName='official_rosetta', notNull=True, default=False) |
593 | remote_product = Unicode( |
594 | name='remote_product', allow_none=True, default=None) |
595 | + max_bug_heat = Int() |
596 | |
597 | def _getMilestoneCondition(self): |
598 | """See `HasMilestonesMixin`.""" |
599 | |
600 | === modified file 'lib/lp/registry/model/productseries.py' |
601 | --- lib/lp/registry/model/productseries.py 2009-12-13 11:55:40 +0000 |
602 | +++ lib/lp/registry/model/productseries.py 2010-02-26 05:16:22 +0000 |
603 | @@ -161,6 +161,11 @@ |
604 | return "%s/%s" % (self.product.name, self.name) |
605 | |
606 | @property |
607 | + def max_bug_heat(self): |
608 | + """See `IHasBugs`.""" |
609 | + return self.product.max_bug_heat |
610 | + |
611 | + @property |
612 | def drivers(self): |
613 | """See IProductSeries.""" |
614 | drivers = set() |
615 | |
616 | === modified file 'lib/lp/registry/model/project.py' |
617 | --- lib/lp/registry/model/project.py 2010-02-17 11:19:42 +0000 |
618 | +++ lib/lp/registry/model/project.py 2010-02-26 05:16:22 +0000 |
619 | @@ -17,6 +17,7 @@ |
620 | from sqlobject import ( |
621 | AND, ForeignKey, StringCol, BoolCol, SQLObjectNotFound) |
622 | from storm.expr import And, In, SQL |
623 | +from storm.locals import Int |
624 | from storm.store import Store |
625 | |
626 | from canonical.database.sqlbase import SQLBase, sqlvalues, quote |
627 | @@ -121,6 +122,7 @@ |
628 | foreignKey="BugTracker", dbName="bugtracker", notNull=False, |
629 | default=None) |
630 | bug_reporting_guidelines = StringCol(default=None) |
631 | + max_bug_heat = Int() |
632 | |
633 | # convenient joins |
634 | |
635 | |
636 | === modified file 'lib/lp/registry/model/sourcepackage.py' |
637 | --- lib/lp/registry/model/sourcepackage.py 2010-02-24 13:49:17 +0000 |
638 | +++ lib/lp/registry/model/sourcepackage.py 2010-02-26 05:16:22 +0000 |
639 | @@ -443,6 +443,11 @@ |
640 | BugTask.sourcepackagename == self.sourcepackagename), |
641 | user) |
642 | |
643 | + @property |
644 | + def max_bug_heat(self): |
645 | + """See `IHasBugs`.""" |
646 | + return self.distribution_sourcepackage.max_bug_heat |
647 | + |
648 | def createBug(self, bug_params): |
649 | """See canonical.launchpad.interfaces.IBugTarget.""" |
650 | # We don't currently support opening a new bug directly on an |
This is the first step in fixing Bug #511382. This branch provides a max_heat attribute on bug targets and a setMaxHeat method. This branch includes the DB patch and a test to exercise the new attribute on each bug target.
I did a implementation call with Björn to make sure the approach was sane.
To run the test:
./bin/test -cvvt test_bugheat
The branch doesn't use max_heat yet. This branch is only to provide the DB changes. A later branch will need to be provided to update this value in an offline-run script and then convert the icon view to use a target's max_heat, rather than the constant MAX_HEAT.