Merge lp:~abentley/launchpad/daily-builds-ui into lp:launchpad
- daily-builds-ui
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Merge reported by: | Aaron Bentley | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~abentley/launchpad/daily-builds-ui | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~abentley/launchpad/daily-builds-score | ||||
Diff against target: |
1420 lines (+201/-632) 32 files modified
Makefile (+1/-1) cronscripts/calculate-bug-heat.py (+0/-33) cronscripts/publishing/maintenance-check.py (+1/-1) database/replication/helpers.py (+10/-4) database/replication/new-slave.py (+5/-0) database/schema/comments.sql (+2/-0) database/schema/fti.py (+10/-13) database/schema/patch-2207-60-1.sql (+10/-0) database/schema/patch-2207-61-0.sql (+13/-0) database/schema/patch-2207-62-0.sql (+14/-0) database/schema/security.cfg (+1/-0) database/schema/trusted.sql (+8/-1) lib/canonical/launchpad/scripts/garbo.py (+0/-1) lib/lp/bugs/browser/bugtask.py (+1/-1) lib/lp/bugs/browser/tests/test_bugtask.py (+9/-8) lib/lp/bugs/configure.zcml (+0/-12) lib/lp/bugs/doc/bugtask-status-workflow.txt (+11/-1) lib/lp/bugs/interfaces/bugjob.py (+1/-11) lib/lp/bugs/interfaces/bugtask.py (+11/-2) lib/lp/bugs/model/bug.py (+0/-1) lib/lp/bugs/model/bugheat.py (+0/-54) lib/lp/bugs/scripts/bugheat.py (+0/-108) lib/lp/bugs/scripts/tests/test_bugheat.py (+0/-256) lib/lp/bugs/tests/bugs-emailinterface.txt (+1/-1) lib/lp/bugs/tests/bugtarget-bugcount.txt (+2/-0) lib/lp/bugs/tests/test_bugheat.py (+1/-102) lib/lp/code/browser/sourcepackagerecipe.py (+13/-2) lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+39/-10) lib/lp/code/configure.zcml (+1/-0) lib/lp/code/interfaces/sourcepackagerecipe.py (+1/-1) lib/lp/code/templates/sourcepackagerecipe-index.pt (+13/-0) utilities/report-database-stats.py (+22/-8) |
||||
To merge this branch: | bzr merge lp:~abentley/launchpad/daily-builds-ui | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Review via email: mp+26334@code.launchpad.net |
Commit message
Provide UI for daily builds
Description of the change
= Summary =
Fix bug #586944: Launchpad should provide a ui for daily builds.
== Proposed fix ==
Allow users to specify that a recipe should be built daily using the web UI.
== Pre-implementation notes ==
None
== Implementation details ==
Allow user to control build_daily and daily_build_archive settings
== Tests ==
bin/test -t test_create_
== Demo and Q/A ==
Create a recipe. Enable daily builds. Select an archive of your choice.
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files:
cronscripts/
lib/lp/
lib/lp/
lib/lp/
lib/canonical
lib/lp/
lib/lp/
database/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
configs/
lib/lp/
lib/lp/
lib/lp/
== Pyflakes Doctest notices ==
lib/lp/
689: local variable 'pub_binaries' is assigned to but never used
== Pyflakes notices ==
cronscripts/
19: 'canonical' imported but unused
^^^ fix for circular imports
== Pylint notices ==
cronscripts/
19: [W0611] Unused import canonical
^^^ fix for circular imports
lib/lp/
207: [W0702, SourcePackageRe
^^^ expected; this is a generic error handler.
lib/lp/
150: [C0322, ISourcePackageR
distroserie
^
)
@export_
def requestBuild(
^^^ bogus
lib/lp/
1265: [W0104, Person.addMember] Statement seems to have no effect
^^^ read to force a flush.
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2010-06-11 18:57:02 +0000 |
3 | +++ Makefile 2010-06-14 20:18:33 +0000 |
4 | @@ -250,7 +250,7 @@ |
5 | bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG) |
6 | |
7 | |
8 | -start_librarian: build |
9 | +start_librarian: compile |
10 | bin/start_librarian |
11 | |
12 | stop_librarian: |
13 | |
14 | === removed file 'cronscripts/calculate-bug-heat.py' |
15 | --- cronscripts/calculate-bug-heat.py 2010-04-27 19:48:39 +0000 |
16 | +++ cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000 |
17 | @@ -1,33 +0,0 @@ |
18 | -#!/usr/bin/python -S |
19 | -# |
20 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
21 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
22 | - |
23 | -# pylint: disable-msg=W0403 |
24 | - |
25 | -"""Calculate bug heat.""" |
26 | - |
27 | -__metaclass__ = type |
28 | - |
29 | -import _pythonpath |
30 | - |
31 | -from canonical.launchpad.webapp import errorlog |
32 | - |
33 | -from lp.services.job.runner import JobCronScript |
34 | -from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
35 | - |
36 | - |
37 | -class RunCalculateBugHeat(JobCronScript): |
38 | - """Run BranchScanJob jobs.""" |
39 | - |
40 | - config_name = 'calculate_bug_heat' |
41 | - source_interface = ICalculateBugHeatJobSource |
42 | - |
43 | - def main(self): |
44 | - errorlog.globalErrorUtility.configure(self.config_name) |
45 | - return super(RunCalculateBugHeat, self).main() |
46 | - |
47 | - |
48 | -if __name__ == '__main__': |
49 | - script = RunCalculateBugHeat() |
50 | - script.lock_and_run() |
51 | |
52 | === modified file 'cronscripts/publishing/maintenance-check.py' |
53 | --- cronscripts/publishing/maintenance-check.py 2010-04-23 13:43:19 +0000 |
54 | +++ cronscripts/publishing/maintenance-check.py 2010-06-14 20:18:33 +0000 |
55 | @@ -350,7 +350,7 @@ |
56 | except: |
57 | logging.exception("can not parse line '%s'" % line) |
58 | except urllib2.HTTPError, e: |
59 | - if e.getcode() != 404: |
60 | + if e.code != 404: |
61 | raise |
62 | sys.stderr.write("hints-file: %s gave 404 error\n" % hints_file) |
63 | |
64 | |
65 | === modified file 'database/replication/helpers.py' |
66 | --- database/replication/helpers.py 2010-04-29 12:38:05 +0000 |
67 | +++ database/replication/helpers.py 2010-06-14 20:18:33 +0000 |
68 | @@ -44,7 +44,6 @@ |
69 | ('public', 'nameblacklist'), |
70 | ('public', 'openidconsumerassociation'), |
71 | ('public', 'openidconsumernonce'), |
72 | - ('public', 'oauthnonce'), |
73 | ('public', 'codeimportmachine'), |
74 | ('public', 'scriptactivity'), |
75 | ('public', 'standardshipitrequest'), |
76 | @@ -71,6 +70,8 @@ |
77 | # Database statistics |
78 | 'public.databasetablestats', |
79 | 'public.databasecpustats', |
80 | + # Don't replicate OAuthNonce - too busy and no real gain. |
81 | + 'public.oauthnonce', |
82 | # Ubuntu SSO database. These tables where created manually by ISD |
83 | # and the Launchpad scripts should not mess with them. Eventually |
84 | # these tables will be in a totally separate database. |
85 | @@ -353,6 +354,9 @@ |
86 | |
87 | A replication set must contain all tables linked by foreign key |
88 | reference to the given table, and sequences used to generate keys. |
89 | + Tables and sequences can be added to the IGNORED_TABLES and |
90 | + IGNORED_SEQUENCES lists for cases where we known can safely ignore |
91 | + this restriction. |
92 | |
93 | :param seeds: [(namespace, tablename), ...] |
94 | |
95 | @@ -420,7 +424,8 @@ |
96 | """ % sqlvalues(namespace, tablename)) |
97 | for namespace, tablename in cur.fetchall(): |
98 | key = (namespace, tablename) |
99 | - if key not in tables and key not in pending_tables: |
100 | + if (key not in tables and key not in pending_tables |
101 | + and '%s.%s' % (namespace, tablename) not in IGNORED_TABLES): |
102 | pending_tables.add(key) |
103 | |
104 | # Generate the set of sequences that are linked to any of our set of |
105 | @@ -441,8 +446,9 @@ |
106 | ) AS whatever |
107 | WHERE seq IS NOT NULL; |
108 | """ % sqlvalues(fqn(namespace, tablename), namespace, tablename)) |
109 | - for row in cur.fetchall(): |
110 | - sequences.add(row[0]) |
111 | + for sequence, in cur.fetchall(): |
112 | + if sequence not in IGNORED_SEQUENCES: |
113 | + sequences.add(sequence) |
114 | |
115 | # We can't easily convert the sequence name to (namespace, name) tuples, |
116 | # so we might as well convert the tables to dot notation for consistancy. |
117 | |
118 | === modified file 'database/replication/new-slave.py' |
119 | --- database/replication/new-slave.py 2010-05-19 18:07:56 +0000 |
120 | +++ database/replication/new-slave.py 2010-06-14 20:18:33 +0000 |
121 | @@ -188,6 +188,9 @@ |
122 | |
123 | script += dedent("""\ |
124 | } on error { echo 'Failed.'; exit 1; } |
125 | + |
126 | + echo 'You may need to restart the Slony daemons now. If the first'; |
127 | + echo 'of the following syncs passes then there is no need.'; |
128 | """) |
129 | |
130 | full_sync = [] |
131 | @@ -200,6 +203,7 @@ |
132 | wait for event ( |
133 | origin = @%(nickname)s, confirmed=ALL, |
134 | wait on = @%(nickname)s, timeout=0); |
135 | + echo 'Ok. Replication syncing fine with new node.'; |
136 | """ % {'nickname': nickname})) |
137 | full_sync = '\n'.join(full_sync) |
138 | script += full_sync |
139 | @@ -210,6 +214,7 @@ |
140 | subscribe set ( |
141 | id=%d, provider=@master_node, receiver=@new_node, forward=yes); |
142 | echo 'Waiting for subscribe to start processing.'; |
143 | + echo 'This will block on long running transactions.'; |
144 | sync (id = @master_node); |
145 | wait for event ( |
146 | origin = @master_node, confirmed = ALL, |
147 | |
148 | === modified file 'database/schema/comments.sql' |
149 | --- database/schema/comments.sql 2010-05-27 22:18:16 +0000 |
150 | +++ database/schema/comments.sql 2010-06-14 20:18:33 +0000 |
151 | @@ -1350,6 +1350,7 @@ |
152 | COMMENT ON COLUMN SourcePackageRecipe.owner IS 'The person or team who can edit this recipe.'; |
153 | COMMENT ON COLUMN SourcePackageRecipe.name IS 'The name of the recipe in the web/URL.'; |
154 | COMMENT ON COLUMN SourcePackageRecipe.build_daily IS 'If true, this recipe should be built daily.'; |
155 | +COMMENT ON COLUMN SourcePackageRecipe.is_stale IS 'True if this recipe has not been built since a branch was updated.'; |
156 | |
157 | COMMENT ON COLUMN SourcePackageREcipe.daily_build_archive IS 'The archive to build into for daily builds.'; |
158 | |
159 | @@ -1371,6 +1372,7 @@ |
160 | COMMENT ON COLUMN SourcePackageRecipeBuild.date_first_dispatched IS 'The instant the build was dispatched the first time. This value will not get overridden if the build is retried.'; |
161 | COMMENT ON COLUMN SourcePackageRecipeBuild.requester IS 'Who requested the build.'; |
162 | COMMENT ON COLUMN SourcePackageRecipeBuild.recipe IS 'The recipe being processed.'; |
163 | +COMMENT ON COLUMN SourcePackageRecipeBuild.manifest IS 'The evaluated recipe that was built.'; |
164 | COMMENT ON COLUMN SourcePackageRecipeBuild.archive IS 'The archive the source package will be built in and uploaded to.'; |
165 | COMMENT ON COLUMN SourcePackageRecipeBuild.pocket IS 'The pocket the source package will be built in and uploaded to.'; |
166 | COMMENT ON COLUMN SourcePackageRecipeBuild.dependencies IS 'The missing build dependencies, if any.'; |
167 | |
168 | === modified file 'database/schema/fti.py' |
169 | --- database/schema/fti.py 2010-05-19 18:07:56 +0000 |
170 | +++ database/schema/fti.py 2010-06-14 20:18:33 +0000 |
171 | @@ -14,10 +14,10 @@ |
172 | import _pythonpath |
173 | |
174 | from distutils.version import LooseVersion |
175 | -import sys |
176 | import os.path |
177 | from optparse import OptionParser |
178 | -import popen2 |
179 | +import subprocess |
180 | +import sys |
181 | from tempfile import NamedTemporaryFile |
182 | from textwrap import dedent |
183 | import time |
184 | @@ -319,18 +319,15 @@ |
185 | cmd += ' -h %s' % lp.dbhost |
186 | if options.dbuser: |
187 | cmd += ' -U %s' % options.dbuser |
188 | - p = popen2.Popen4(cmd) |
189 | - c = p.tochild |
190 | - print >> c, "SET client_min_messages=ERROR;" |
191 | - print >> c, "CREATE SCHEMA ts2;" |
192 | - print >> c, open(tsearch2_sql_path).read().replace( |
193 | - 'public;','ts2, public;' |
194 | - ) |
195 | - p.tochild.close() |
196 | - rv = p.wait() |
197 | - if rv != 0: |
198 | + p = subprocess.Popen( |
199 | + cmd.split(' '), stdin=subprocess.PIPE, |
200 | + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
201 | + out, err = p.communicate( |
202 | + "SET client_min_messages=ERROR; CREATE SCHEMA ts2;" |
203 | + + open(tsearch2_sql_path).read().replace('public;','ts2, public;')) |
204 | + if p.returncode != 0: |
205 | log.fatal('Error executing %s:', cmd) |
206 | - log.debug(p.fromchild.read()) |
207 | + log.debug(out) |
208 | sys.exit(rv) |
209 | |
210 | # Create ftq helper and its sibling _ftq. |
211 | |
212 | === added file 'database/schema/patch-2207-60-1.sql' |
213 | --- database/schema/patch-2207-60-1.sql 1970-01-01 00:00:00 +0000 |
214 | +++ database/schema/patch-2207-60-1.sql 2010-06-14 20:18:33 +0000 |
215 | @@ -0,0 +1,10 @@ |
216 | +SET client_min_messages=ERROR; |
217 | + |
218 | +CREATE INDEX archive__require_virtualized__idx |
219 | +ON Archive(require_virtualized); |
220 | + |
221 | +CREATE INDEX buildfarmjob__status__idx |
222 | +ON BuildFarmJob(status); |
223 | + |
224 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 60, 1); |
225 | + |
226 | |
227 | === added file 'database/schema/patch-2207-61-0.sql' |
228 | --- database/schema/patch-2207-61-0.sql 1970-01-01 00:00:00 +0000 |
229 | +++ database/schema/patch-2207-61-0.sql 2010-06-14 20:18:33 +0000 |
230 | @@ -0,0 +1,13 @@ |
231 | +-- Copyright 2010 Canonical Ltd. This software is licensed under the |
232 | +-- GNU Affero General Public License version 3 (see the file LICENSE). |
233 | + |
234 | +SET client_min_messages=ERROR; |
235 | +ALTER TABLE SourcePackageRecipe ADD COLUMN is_stale BOOLEAN NOT NULL DEFAULT TRUE; |
236 | +ALTER TABLE SourcePackageRecipeBuild ADD COLUMN manifest INTEGER REFERENCES SourcePackageRecipeData; |
237 | + |
238 | +CREATE INDEX sourcepackagerecipe__is_stale__build_daily__idx |
239 | +ON SourcepackageRecipe(is_stale, build_daily); |
240 | + |
241 | +CREATE INDEX sourcepackagerecipebuild__manifest__idx ON SourcepackageRecipeBuild(manifest); |
242 | + |
243 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 61, 0); |
244 | |
245 | === added file 'database/schema/patch-2207-62-0.sql' |
246 | --- database/schema/patch-2207-62-0.sql 1970-01-01 00:00:00 +0000 |
247 | +++ database/schema/patch-2207-62-0.sql 2010-06-14 20:18:33 +0000 |
248 | @@ -0,0 +1,14 @@ |
249 | +SET client_min_messages=ERROR; |
250 | + |
251 | +-- Bug #49717 |
252 | +ALTER TABLE SourcePackageRelease ALTER component SET NOT NULL; |
253 | + |
254 | +-- We are taking OAuthNonce out of replication, so we make the foreign |
255 | +-- key reference ON DELETE CASCADE so things don't explode when we |
256 | +-- shuffle the lpmain master around. |
257 | +ALTER TABLE OAuthNonce DROP CONSTRAINT oauthnonce__access_token__fk; |
258 | +ALTER TABLE OAuthNonce ADD CONSTRAINT oauthnonce__access_token__fk |
259 | + FOREIGN KEY (access_token) REFERENCES OAuthAccessToken |
260 | + ON DELETE CASCADE; |
261 | + |
262 | +INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 62, 0); |
263 | |
264 | === modified file 'database/schema/security.cfg' |
265 | --- database/schema/security.cfg 2010-06-08 15:13:20 +0000 |
266 | +++ database/schema/security.cfg 2010-06-14 20:18:33 +0000 |
267 | @@ -1847,6 +1847,7 @@ |
268 | type=user |
269 | public.archive = SELECT |
270 | public.buildfarmjob = SELECT |
271 | +public.databasereplicationlag = SELECT |
272 | public.packagebuild = SELECT |
273 | public.binarypackagebuild = SELECT |
274 | public.buildqueue = SELECT |
275 | |
276 | === modified file 'database/schema/trusted.sql' |
277 | --- database/schema/trusted.sql 2010-05-28 10:36:08 +0000 |
278 | +++ database/schema/trusted.sql 2010-06-14 20:18:33 +0000 |
279 | @@ -144,6 +144,12 @@ |
280 | LIMIT 1 |
281 | """, 1).nrows() > 0 |
282 | if stats_reset: |
283 | + # The database stats have been reset. We cannot calculate |
284 | + # deltas because we do not know when this happened. So we trash |
285 | + # our records as they are now useless to us. We could be more |
286 | + # sophisticated about this, but this should only happen |
287 | + # when an admin explicitly resets the statistics or if the |
288 | + # database is rebuilt. |
289 | plpy.notice("Stats wraparound. Purging DatabaseTableStats") |
290 | plpy.execute("DELETE FROM DatabaseTableStats") |
291 | else: |
292 | @@ -158,7 +164,8 @@ |
293 | SELECT |
294 | CURRENT_TIMESTAMP AT TIME ZONE 'UTC', |
295 | schemaname, relname, seq_scan, seq_tup_read, |
296 | - idx_scan, idx_tup_fetch, n_tup_ins, n_tup_upd, n_tup_del, |
297 | + coalesce(idx_scan, 0), coalesce(idx_tup_fetch, 0), |
298 | + n_tup_ins, n_tup_upd, n_tup_del, |
299 | n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum, |
300 | last_autovacuum, last_analyze, last_autoanalyze |
301 | FROM pg_catalog.pg_stat_user_tables; |
302 | |
303 | === modified file 'lib/canonical/launchpad/scripts/garbo.py' |
304 | --- lib/canonical/launchpad/scripts/garbo.py 2010-06-11 07:26:03 +0000 |
305 | +++ lib/canonical/launchpad/scripts/garbo.py 2010-06-14 20:18:33 +0000 |
306 | @@ -33,7 +33,6 @@ |
307 | from lp.bugs.interfaces.bug import IBugSet |
308 | from lp.bugs.model.bug import Bug |
309 | from lp.bugs.model.bugattachment import BugAttachment |
310 | -from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
311 | from lp.bugs.model.bugnotification import BugNotification |
312 | from lp.bugs.model.bugwatch import BugWatch |
313 | from lp.bugs.scripts.checkwatches.scheduler import ( |
314 | |
315 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
316 | --- lib/lp/bugs/browser/bugtask.py 2010-05-25 16:45:26 +0000 |
317 | +++ lib/lp/bugs/browser/bugtask.py 2010-06-14 20:18:33 +0000 |
318 | @@ -2646,7 +2646,7 @@ |
319 | dict( |
320 | value=term.token, title=term.title or term.token, |
321 | checked=term.value in default_values)) |
322 | - return helpers.shortlist(widget_values, longest_expected=11) |
323 | + return helpers.shortlist(widget_values, longest_expected=12) |
324 | |
325 | def getStatusWidgetValues(self): |
326 | """Return data used to render the status checkboxes.""" |
327 | |
328 | === modified file 'lib/lp/bugs/browser/tests/test_bugtask.py' |
329 | --- lib/lp/bugs/browser/tests/test_bugtask.py 2010-05-25 14:50:42 +0000 |
330 | +++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-06-14 20:18:33 +0000 |
331 | @@ -245,8 +245,8 @@ |
332 | self.bug.default_bugtask, LaunchpadTestRequest()) |
333 | view.initialize() |
334 | self.assertEqual( |
335 | - ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress', |
336 | - 'Fix Committed', 'Fix Released'], |
337 | + ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed', |
338 | + 'In Progress', 'Fix Committed', 'Fix Released'], |
339 | self.getWidgetOptionTitles(view.form_fields['status'])) |
340 | |
341 | def test_status_field_privileged_persons(self): |
342 | @@ -260,8 +260,9 @@ |
343 | self.bug.default_bugtask, LaunchpadTestRequest()) |
344 | view.initialize() |
345 | self.assertEqual( |
346 | - ['New', 'Incomplete', 'Invalid', "Won't Fix", 'Confirmed', |
347 | - 'Triaged', 'In Progress', 'Fix Committed', 'Fix Released'], |
348 | + ['New', 'Incomplete', 'Opinion', 'Invalid', "Won't Fix", |
349 | + 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed', |
350 | + 'Fix Released'], |
351 | self.getWidgetOptionTitles(view.form_fields['status']), |
352 | 'Unexpected set of settable status options for %s' |
353 | % user.name) |
354 | @@ -278,8 +279,8 @@ |
355 | self.bug.default_bugtask, LaunchpadTestRequest()) |
356 | view.initialize() |
357 | self.assertEqual( |
358 | - ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress', |
359 | - 'Fix Committed', 'Fix Released', 'Unknown'], |
360 | + ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed', |
361 | + 'In Progress', 'Fix Committed', 'Fix Released', 'Unknown'], |
362 | self.getWidgetOptionTitles(view.form_fields['status'])) |
363 | |
364 | def test_status_field_bug_task_in_status_expired(self): |
365 | @@ -292,8 +293,8 @@ |
366 | self.bug.default_bugtask, LaunchpadTestRequest()) |
367 | view.initialize() |
368 | self.assertEqual( |
369 | - ['New', 'Incomplete', 'Invalid', 'Expired', 'Confirmed', |
370 | - 'In Progress', 'Fix Committed', 'Fix Released'], |
371 | + ['New', 'Incomplete', 'Opinion', 'Invalid', 'Expired', |
372 | + 'Confirmed', 'In Progress', 'Fix Committed', 'Fix Released'], |
373 | self.getWidgetOptionTitles(view.form_fields['status'])) |
374 | |
375 | |
376 | |
377 | === modified file 'lib/lp/bugs/configure.zcml' |
378 | --- lib/lp/bugs/configure.zcml 2010-06-04 09:31:21 +0000 |
379 | +++ lib/lp/bugs/configure.zcml 2010-06-14 20:18:33 +0000 |
380 | @@ -969,18 +969,6 @@ |
381 | factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb" |
382 | permission="zope.Public"/> |
383 | |
384 | - <!-- CalculateBugHeatJobs --> |
385 | - <class class="lp.bugs.model.bugheat.CalculateBugHeatJob"> |
386 | - <allow interface="lp.bugs.interfaces.bugjob.IBugJob" /> |
387 | - <allow interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJob"/> |
388 | - </class> |
389 | - <securedutility |
390 | - component="lp.bugs.model.bugheat.CalculateBugHeatJob" |
391 | - provides="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"> |
392 | - <allow |
393 | - interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"/> |
394 | - </securedutility> |
395 | - |
396 | <!-- ProcessApportBlobJobs --> |
397 | <class class="lp.bugs.model.apportjob.ProcessApportBlobJob"> |
398 | <allow interface="lp.bugs.interfaces.apportjob.IApportJob" /> |
399 | |
400 | === modified file 'lib/lp/bugs/doc/bugtask-status-workflow.txt' |
401 | --- lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-04-15 15:28:22 +0000 |
402 | +++ lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-06-14 20:18:33 +0000 |
403 | @@ -145,7 +145,7 @@ |
404 | >>> ubuntu_firefox_task.date_inprogress is None |
405 | True |
406 | |
407 | -Marking the bug Triaged sets `date_triged`. |
408 | +Marking the bug Triaged sets `date_triaged`. |
409 | |
410 | >>> print ubuntu_firefox_task.date_triaged |
411 | None |
412 | @@ -188,6 +188,16 @@ |
413 | |
414 | >>> ubuntu_firefox_task.transitionToStatus( |
415 | ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user) |
416 | + >>> ubuntu_firefox_task.date_closed is None |
417 | + True |
418 | + |
419 | + >>> ubuntu_firefox_task.transitionToStatus( |
420 | + ... BugTaskStatus.OPINION, getUtility(ILaunchBag).user) |
421 | + >>> ubuntu_firefox_task.date_closed |
422 | + datetime.datetime... |
423 | + |
424 | + >>> ubuntu_firefox_task.transitionToStatus( |
425 | + ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user) |
426 | >>> ubuntu_firefox_task.date_inprogress is None |
427 | True |
428 | >>> ubuntu_firefox_task.transitionToStatus( |
429 | |
430 | === modified file 'lib/lp/bugs/interfaces/bugjob.py' |
431 | --- lib/lp/bugs/interfaces/bugjob.py 2010-01-22 21:44:19 +0000 |
432 | +++ lib/lp/bugs/interfaces/bugjob.py 2010-06-14 20:18:33 +0000 |
433 | @@ -8,8 +8,6 @@ |
434 | 'BugJobType', |
435 | 'IBugJob', |
436 | 'IBugJobSource', |
437 | - 'ICalculateBugHeatJob', |
438 | - 'ICalculateBugHeatJobSource', |
439 | ] |
440 | |
441 | from zope.interface import Attribute, Interface |
442 | @@ -19,7 +17,7 @@ |
443 | |
444 | from lazr.enum import DBEnumeratedType, DBItem |
445 | from lp.bugs.interfaces.bug import IBug |
446 | -from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob |
447 | +from lp.services.job.interfaces.job import IJob, IJobSource |
448 | |
449 | |
450 | class BugJobType(DBEnumeratedType): |
451 | @@ -57,11 +55,3 @@ |
452 | |
453 | def create(bug): |
454 | """Create a new IBugJob for a bug.""" |
455 | - |
456 | - |
457 | -class ICalculateBugHeatJob(IRunnableJob): |
458 | - """A Job to calculate bug heat.""" |
459 | - |
460 | - |
461 | -class ICalculateBugHeatJobSource(IBugJobSource): |
462 | - """Interface for acquiring CalculateBugHeatJobs.""" |
463 | |
464 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
465 | --- lib/lp/bugs/interfaces/bugtask.py 2010-06-07 19:48:29 +0000 |
466 | +++ lib/lp/bugs/interfaces/bugtask.py 2010-06-14 20:18:33 +0000 |
467 | @@ -159,6 +159,14 @@ |
468 | the user was visiting when the bug occurred, etc. |
469 | """) |
470 | |
471 | + OPINION = DBItem(16, """ |
472 | + Opinion |
473 | + |
474 | + The bug remains open for discussion only. This status is usually |
475 | + used where there is disagreement over whether the bug is relevant |
476 | + to the current target and whether it should be fixed. |
477 | + """) |
478 | + |
479 | INVALID = DBItem(17, """ |
480 | Invalid |
481 | |
482 | @@ -235,8 +243,8 @@ |
483 | |
484 | sort_order = ( |
485 | 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE', |
486 | - 'INCOMPLETE', 'INVALID', 'WONTFIX', 'EXPIRED', 'CONFIRMED', 'TRIAGED', |
487 | - 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED') |
488 | + 'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED', |
489 | + 'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED') |
490 | |
491 | INCOMPLETE_WITH_RESPONSE = DBItem(35, """ |
492 | Incomplete (with response) |
493 | @@ -312,6 +320,7 @@ |
494 | |
495 | RESOLVED_BUGTASK_STATUSES = ( |
496 | BugTaskStatus.FIXRELEASED, |
497 | + BugTaskStatus.OPINION, |
498 | BugTaskStatus.INVALID, |
499 | BugTaskStatus.WONTFIX, |
500 | BugTaskStatus.EXPIRED) |
501 | |
502 | === modified file 'lib/lp/bugs/model/bug.py' |
503 | --- lib/lp/bugs/model/bug.py 2010-06-10 18:55:22 +0000 |
504 | +++ lib/lp/bugs/model/bug.py 2010-06-14 20:18:33 +0000 |
505 | @@ -83,7 +83,6 @@ |
506 | from lp.bugs.interfaces.bugtracker import BugTrackerType |
507 | from lp.bugs.interfaces.bugwatch import IBugWatchSet |
508 | from lp.bugs.interfaces.cve import ICveSet |
509 | -from lp.bugs.scripts.bugheat import BugHeatConstants |
510 | from lp.bugs.model.bugattachment import BugAttachment |
511 | from lp.bugs.model.bugbranch import BugBranch |
512 | from lp.bugs.model.bugcve import BugCve |
513 | |
514 | === removed file 'lib/lp/bugs/model/bugheat.py' |
515 | --- lib/lp/bugs/model/bugheat.py 2010-01-21 20:46:03 +0000 |
516 | +++ lib/lp/bugs/model/bugheat.py 1970-01-01 00:00:00 +0000 |
517 | @@ -1,54 +0,0 @@ |
518 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
519 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
520 | - |
521 | -"""Job classes related to BugJobs are in here.""" |
522 | - |
523 | -__metaclass__ = type |
524 | -__all__ = [ |
525 | - 'CalculateBugHeatJob', |
526 | - ] |
527 | - |
528 | -from zope.component import getUtility |
529 | -from zope.interface import classProvides, implements |
530 | - |
531 | -from canonical.launchpad.webapp.interfaces import ( |
532 | - DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE) |
533 | - |
534 | -from lp.bugs.interfaces.bugjob import ( |
535 | - BugJobType, ICalculateBugHeatJob, ICalculateBugHeatJobSource) |
536 | -from lp.bugs.model.bugjob import BugJob, BugJobDerived |
537 | -from lp.bugs.scripts.bugheat import BugHeatCalculator |
538 | -from lp.services.job.model.job import Job |
539 | - |
540 | - |
541 | -class CalculateBugHeatJob(BugJobDerived): |
542 | - """A Job to calculate bug heat.""" |
543 | - implements(ICalculateBugHeatJob) |
544 | - |
545 | - class_job_type = BugJobType.UPDATE_HEAT |
546 | - classProvides(ICalculateBugHeatJobSource) |
547 | - |
548 | - def run(self): |
549 | - """See `IRunnableJob`.""" |
550 | - calculator = BugHeatCalculator(self.bug) |
551 | - calculated_heat = calculator.getBugHeat() |
552 | - self.bug.setHeat(calculated_heat) |
553 | - |
554 | - @classmethod |
555 | - def create(cls, bug): |
556 | - """See `ICalculateBugHeatJobSource`.""" |
557 | - # If there's already a job for the bug, don't create a new one. |
558 | - store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR) |
559 | - job_for_bug = store.find( |
560 | - BugJob, |
561 | - BugJob.bug == bug, |
562 | - BugJob.job_type == cls.class_job_type, |
563 | - BugJob.job == Job.id, |
564 | - Job.id.is_in(Job.ready_jobs) |
565 | - ).any() |
566 | - |
567 | - if job_for_bug is not None: |
568 | - return cls(job_for_bug) |
569 | - else: |
570 | - return super(CalculateBugHeatJob, cls).create(bug) |
571 | - |
572 | |
573 | === removed file 'lib/lp/bugs/scripts/bugheat.py' |
574 | --- lib/lp/bugs/scripts/bugheat.py 2010-04-29 11:31:49 +0000 |
575 | +++ lib/lp/bugs/scripts/bugheat.py 1970-01-01 00:00:00 +0000 |
576 | @@ -1,108 +0,0 @@ |
577 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
578 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
579 | - |
580 | -"""The innards of the Bug Heat cronscript.""" |
581 | - |
582 | -__metaclass__ = type |
583 | -__all__ = [ |
584 | - 'BugHeatCalculator', |
585 | - 'BugHeatConstants', |
586 | - ] |
587 | - |
588 | -from datetime import datetime |
589 | - |
590 | -from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES |
591 | - |
592 | -class BugHeatConstants: |
593 | - |
594 | - PRIVACY = 150 |
595 | - SECURITY = 250 |
596 | - DUPLICATE = 6 |
597 | - AFFECTED_USER = 4 |
598 | - SUBSCRIBER = 2 |
599 | - |
600 | - |
601 | -class BugHeatCalculator: |
602 | - """A class to calculate the heat for a bug.""" |
603 | - # If you change the way that bug heat is calculated, remember to update |
604 | - # the description of how it is calculated at |
605 | - # /lib/lp/bugs/help/bug-heat.html and |
606 | - # https://help.launchpad.net/Bugs/BugHeat |
607 | - |
608 | - def __init__(self, bug): |
609 | - self.bug = bug |
610 | - |
611 | - def _getHeatFromPrivacy(self): |
612 | - """Return the heat generated by the bug's `private` attribute.""" |
613 | - if self.bug.private: |
614 | - return BugHeatConstants.PRIVACY |
615 | - else: |
616 | - return 0 |
617 | - |
618 | - def _getHeatFromSecurity(self): |
619 | - """Return the heat generated if the bug is security related.""" |
620 | - if self.bug.security_related: |
621 | - return BugHeatConstants.SECURITY |
622 | - else: |
623 | - return 0 |
624 | - |
625 | - def _getHeatFromDuplicates(self): |
626 | - """Return the heat generated by the bug's duplicates.""" |
627 | - return self.bug.duplicates.count() * BugHeatConstants.DUPLICATE |
628 | - |
629 | - def _getHeatFromAffectedUsers(self): |
630 | - """Return the heat generated by the bug's affected users.""" |
631 | - return ( |
632 | - self.bug.users_affected_count_with_dupes * |
633 | - BugHeatConstants.AFFECTED_USER) |
634 | - |
635 | - def _getHeatFromSubscribers(self): |
636 | - """Return the heat generated by the bug's subscribers.""" |
637 | - direct_subscribers = self.bug.getDirectSubscribers() |
638 | - subscribers_from_dupes = self.bug.getSubscribersFromDuplicates() |
639 | - |
640 | - subscriber_count = ( |
641 | - len(direct_subscribers) + len(subscribers_from_dupes)) |
642 | - return subscriber_count * BugHeatConstants.SUBSCRIBER |
643 | - |
644 | - def _bugIsComplete(self): |
645 | - """Are all the tasks for this bug resolved?""" |
646 | - return all([(task.status in RESOLVED_BUGTASK_STATUSES) |
647 | - for task in self.bug.bugtasks]) |
648 | - |
649 | - def getBugHeat(self): |
650 | - """Return the total heat for the current bug.""" |
651 | - if self._bugIsComplete(): |
652 | - return 0 |
653 | - |
654 | - total_heat = sum([ |
655 | - self._getHeatFromAffectedUsers(), |
656 | - self._getHeatFromDuplicates(), |
657 | - self._getHeatFromPrivacy(), |
658 | - self._getHeatFromSecurity(), |
659 | - self._getHeatFromSubscribers(), |
660 | - ]) |
661 | - |
662 | - # Bugs decay over time. Every day the bug isn't touched its heat |
663 | - # decreases by 1%. |
664 | - days = ( |
665 | - datetime.utcnow() - |
666 | - self.bug.date_last_updated.replace(tzinfo=None)).days |
667 | - total_heat = int(total_heat * (0.99 ** days)) |
668 | - |
669 | - if days > 0: |
670 | - # Bug heat increases by a quarter of the maximum bug heat divided |
671 | - # by the number of days since the bug's creation date. |
672 | - days_since_last_activity = ( |
673 | - datetime.utcnow() - |
674 | - max(self.bug.date_last_updated.replace(tzinfo=None), |
675 | - self.bug.date_last_message.replace(tzinfo=None))).days |
676 | - days_since_created = ( |
677 | - datetime.utcnow() - self.bug.datecreated.replace(tzinfo=None)).days |
678 | - max_heat = max( |
679 | - task.target.max_bug_heat for task in self.bug.bugtasks) |
680 | - if max_heat is not None and days_since_created > 0: |
681 | - total_heat = total_heat + (max_heat * 0.25 / days_since_created) |
682 | - |
683 | - return int(total_heat) |
684 | - |
685 | |
686 | === removed file 'lib/lp/bugs/scripts/tests/test_bugheat.py' |
687 | --- lib/lp/bugs/scripts/tests/test_bugheat.py 2010-04-29 11:31:49 +0000 |
688 | +++ lib/lp/bugs/scripts/tests/test_bugheat.py 1970-01-01 00:00:00 +0000 |
689 | @@ -1,256 +0,0 @@ |
690 | -# Copyright 2010 Canonical Ltd. This software is licensed under the |
691 | -# GNU Affero General Public License version 3 (see the file LICENSE). |
692 | - |
693 | -"""Module docstring goes here.""" |
694 | - |
695 | -__metaclass__ = type |
696 | - |
697 | -import unittest |
698 | - |
699 | -from datetime import datetime, timedelta |
700 | - |
701 | -from canonical.testing import LaunchpadZopelessLayer |
702 | - |
703 | -from lp.bugs.interfaces.bugtask import BugTaskStatus |
704 | -from lp.bugs.scripts.bugheat import BugHeatCalculator, BugHeatConstants |
705 | -from lp.testing import TestCaseWithFactory |
706 | - |
707 | -from zope.security.proxy import removeSecurityProxy |
708 | - |
709 | - |
710 | -class TestBugHeatCalculator(TestCaseWithFactory): |
711 | - """Tests for the BugHeatCalculator class.""" |
712 | - # If you change the way that bug heat is calculated, remember to update |
713 | - # the description of how it is calculated at |
714 | - # /lib/lp/bugs/help/bug-heat.html and |
715 | - # https://help.launchpad.net/Bugs/BugHeat |
716 | - |
717 | - layer = LaunchpadZopelessLayer |
718 | - |
719 | - def setUp(self): |
720 | - super(TestBugHeatCalculator, self).setUp() |
721 | - self.bug = self.factory.makeBug() |
722 | - self.calculator = BugHeatCalculator(self.bug) |
723 | - |
724 | - def test__getHeatFromDuplicates(self): |
725 | - # BugHeatCalculator._getHeatFromDuplicates() returns the bug |
726 | - # heat generated by duplicates of a bug. |
727 | - # By default, the bug has no heat from dupes |
728 | - self.assertEqual(0, self.calculator._getHeatFromDuplicates()) |
729 | - |
730 | - # If adding duplicates, the heat generated by them will be n * |
731 | - # BugHeatConstants.DUPLICATE, where n is the number of |
732 | - # duplicates. |
733 | - for i in range(5): |
734 | - dupe = self.factory.makeBug() |
735 | - dupe.duplicateof = self.bug |
736 | - |
737 | - expected_heat = BugHeatConstants.DUPLICATE * 5 |
738 | - actual_heat = self.calculator._getHeatFromDuplicates() |
739 | - self.assertEqual( |
740 | - expected_heat, actual_heat, |
741 | - "Heat from duplicates does not match expected heat. " |
742 | - "Expected %s, got %s" % (expected_heat, actual_heat)) |
743 | - |
744 | - def test__getHeatFromAffectedUsers(self): |
745 | - # BugHeatCalculator._getHeatFromAffectedUsers() returns the bug |
746 | - # heat generated by users affected by the bug and by duplicate bugs. |
747 | - # By default, the heat will be BugHeatConstants.AFFECTED_USER, since |
748 | - # there will be one affected user (the user who filed the bug). |
749 | - self.assertEqual( |
750 | - BugHeatConstants.AFFECTED_USER, |
751 | - self.calculator._getHeatFromAffectedUsers()) |
752 | - |
753 | - # As the number of affected users increases, the heat generated |
754 | - # will be n * BugHeatConstants.AFFECTED_USER, where n is the number |
755 | - # of affected users. |
756 | - for i in range(5): |
757 | - person = self.factory.makePerson() |
758 | - self.bug.markUserAffected(person) |
759 | - |
760 | - expected_heat = BugHeatConstants.AFFECTED_USER * 6 |
761 | - actual_heat = self.calculator._getHeatFromAffectedUsers() |
762 | - self.assertEqual( |
763 | - expected_heat, actual_heat, |
764 | - "Heat from affected users does not match expected heat. " |
765 | - "Expected %s, got %s" % (expected_heat, actual_heat)) |
766 | - |
767 | - # When our bug has duplicates, users affected by these duplicates |
768 | - # are included in _getHeatFromAffectedUsers() of the main bug. |
769 | - for i in range(3): |
770 | - dupe = self.factory.makeBug() |
771 | - dupe.duplicateof = self.bug |
772 | - # Each bug reporter is by default also marked as being affected |
773 | - # by the bug, so we have three additional affected users. |
774 | - expected_heat += BugHeatConstants.AFFECTED_USER * 3 |
775 | - |
776 | - person = self.factory.makePerson() |
777 | - dupe.markUserAffected(person) |
778 | - expected_heat += BugHeatConstants.AFFECTED_USER |
779 | - actual_heat = self.calculator._getHeatFromAffectedUsers() |
780 | - self.assertEqual( |
781 | - expected_heat, actual_heat, |
782 | - "Heat from users affected by duplicate bugs does not match " |
783 | - "expected heat. Expected %s, got %s" |
784 | - % (expected_heat, actual_heat)) |
785 | - |
786 | - def test__getHeatFromSubscribers(self): |
787 | - # BugHeatCalculator._getHeatFromSubscribers() returns the bug |
788 | - # heat generated by users subscribed tothe bug. |
789 | - # By default, the heat will be BugHeatConstants.SUBSCRIBER, |
790 | - # since there will be one direct subscriber (the user who filed |
791 | - # the bug). |
792 | - self.assertEqual( |
793 | - BugHeatConstants.SUBSCRIBER, |
794 | - self.calculator._getHeatFromSubscribers()) |
795 | - |
796 | - # As the number of subscribers increases, the heat generated |
797 | - # will be n * BugHeatConstants.SUBSCRIBER, where n is the number |
798 | - # of subscribers. |
799 | - for i in range(5): |
800 | - person = self.factory.makePerson() |
801 | - self.bug.subscribe(person, person) |
802 | - |
803 | - expected_heat = BugHeatConstants.SUBSCRIBER * 6 |
804 | - actual_heat = self.calculator._getHeatFromSubscribers() |
805 | - self.assertEqual( |
806 | - expected_heat, actual_heat, |
807 | - "Heat from subscribers does not match expected heat. " |
808 | - "Expected %s, got %s" % (expected_heat, actual_heat)) |
809 | - |
810 | - # Subscribers from duplicates are included in the heat returned |
811 | - # by _getHeatFromSubscribers() |
812 | - dupe = self.factory.makeBug() |
813 | - dupe.duplicateof = self.bug |
814 | - expected_heat = BugHeatConstants.SUBSCRIBER * 7 |
815 | - actual_heat = self.calculator._getHeatFromSubscribers() |
816 | - self.assertEqual( |
817 | - expected_heat, actual_heat, |
818 | - "Heat from subscribers (including duplicate-subscribers) " |
819 | - "does not match expected heat. Expected %s, got %s" % |
820 | - (expected_heat, actual_heat)) |
821 | - |
822 | - # Seting the bug to private will increase its heat from |
823 | - # subscribers by 1 * BugHeatConstants.SUBSCRIBER, as the project |
824 | - # owner will now be directly subscribed to it. |
825 | - self.bug.setPrivate(True, self.bug.owner) |
826 | - expected_heat = BugHeatConstants.SUBSCRIBER * 8 |
827 | - actual_heat = self.calculator._getHeatFromSubscribers() |
828 | - self.assertEqual( |
829 | - expected_heat, actual_heat, |
830 | - "Heat from subscribers to private bug does not match expected " |
831 | - "heat. Expected %s, got %s" % (expected_heat, actual_heat)) |
832 | - |
833 | - def test__getHeatFromPrivacy(self): |
834 | - # BugHeatCalculator._getHeatFromPrivacy() returns the heat |
835 | - # generated by the bug's private attribute. If the bug is |
836 | - # public, this will be 0. |
837 | - self.assertEqual(0, self.calculator._getHeatFromPrivacy()) |
838 | - |
839 | - # However, if the bug is private, _getHeatFromPrivacy() will |
840 | - # return BugHeatConstants.PRIVACY. |
841 | - self.bug.setPrivate(True, self.bug.owner) |
842 | - self.assertEqual( |
843 | - BugHeatConstants.PRIVACY, self.calculator._getHeatFromPrivacy()) |
844 | - |
845 | - def test__getHeatFromSecurity(self): |
846 | - # BugHeatCalculator._getHeatFromSecurity() returns the heat |
847 | - # generated by the bug's security_related attribute. If the bug |
848 | - # is not security related, _getHeatFromSecurity() will return 0. |
849 | - self.assertEqual(0, self.calculator._getHeatFromPrivacy()) |
850 | - |
851 | - |
852 | - # If, on the other hand, the bug is security_related, |
853 | - # _getHeatFromSecurity() will return BugHeatConstants.SECURITY |
854 | - self.bug.setSecurityRelated(True) |
855 | - self.assertEqual( |
856 | - BugHeatConstants.SECURITY, self.calculator._getHeatFromSecurity()) |
857 | - |
858 | - def test_getBugHeat(self): |
859 | - # BugHeatCalculator.getBugHeat() returns the total heat for a |
860 | - # given bug as the sum of the results of all _getHeatFrom*() |
861 | - # methods. |
862 | - # By default this will be (BugHeatConstants.AFFECTED_USER + |
863 | - # BugHeatConstants.SUBSCRIBER) since there will be one |
864 | - # subscriber and one affected user only. |
865 | - expected_heat = ( |
866 | - BugHeatConstants.AFFECTED_USER + BugHeatConstants.SUBSCRIBER) |
867 | - actual_heat = self.calculator.getBugHeat() |
868 | - self.assertEqual( |
869 | - expected_heat, actual_heat, |
870 | - "Expected bug heat did not match actual bug heat. " |
871 | - "Expected %s, got %s" % (expected_heat, actual_heat)) |
872 | - |
873 | - # Adding a duplicate and making the bug private and security |
874 | - # related will increase its heat. |
875 | - dupe = self.factory.makeBug() |
876 | - dupe.duplicateof = self.bug |
877 | - self.bug.setPrivate(True, self.bug.owner) |
878 | - self.bug.setSecurityRelated(True) |
879 | - |
880 | - expected_heat += ( |
881 | - BugHeatConstants.DUPLICATE + |
882 | - BugHeatConstants.PRIVACY + |
883 | - BugHeatConstants.SECURITY + |
884 | - BugHeatConstants.AFFECTED_USER |
885 | - ) |
886 | - |
887 | - # Adding the duplicate and making the bug private means it gets |
888 | - # two new subscribers, the project owner and the duplicate's |
889 | - # direct subscriber. |
890 | - expected_heat += BugHeatConstants.SUBSCRIBER * 2 |
891 | - actual_heat = self.calculator.getBugHeat() |
892 | - self.assertEqual( |
893 | - expected_heat, actual_heat, |
894 | - "Expected bug heat did not match actual bug heat. " |
895 | - "Expected %s, got %s" % (expected_heat, actual_heat)) |
896 | - |
897 | - def test_getBugHeat_complete_bugs(self): |
898 | - # Bug which are in a resolved status don't have heat at all. |
899 | - complete_bug = self.factory.makeBug() |
900 | - heat = BugHeatCalculator(complete_bug).getBugHeat() |
901 | - self.assertNotEqual( |
902 | - 0, heat, |
903 | - "Expected bug heat did not match actual bug heat. " |
904 | - "Expected a positive value, got 0") |
905 | - complete_bug.bugtasks[0].transitionToStatus( |
906 | - BugTaskStatus.INVALID, complete_bug.owner) |
907 | - heat = BugHeatCalculator(complete_bug).getBugHeat() |
908 | - self.assertEqual( |
909 | - 0, heat, |
910 | - "Expected bug heat did not match actual bug heat. " |
911 | - "Expected %s, got %s" % (0, heat)) |
912 | - |
913 | - def test_getBugHeat_decay(self): |
914 | - # Every day, a bug that wasn't touched has its heat reduced by 1%. |
915 | - aging_bug = self.factory.makeBug() |
916 | - fresh_heat = BugHeatCalculator(aging_bug).getBugHeat() |
917 | - aging_bug.date_last_updated = ( |
918 | - aging_bug.date_last_updated - timedelta(days=1)) |
919 | - expected = int(fresh_heat * 0.99) |
920 | - heat = BugHeatCalculator(aging_bug).getBugHeat() |
921 | - self.assertEqual( |
922 | - expected, heat, |
923 | - "Expected bug heat did not match actual bug heat. " |
924 | - "Expected %s, got %s" % (expected, heat)) |
925 | - |
926 | - def test_getBugHeat_activity(self): |
927 | - # Bug heat increases by a quarter of the maximum bug heat divided by |
928 | - # the number of days between the bug's creating and its last activity. |
929 | - active_bug = removeSecurityProxy(self.factory.makeBug()) |
930 | - fresh_heat = BugHeatCalculator(active_bug).getBugHeat() |
931 | - active_bug.date_last_updated = ( |
932 | - active_bug.date_last_updated - timedelta(days=10)) |
933 | - active_bug.datecreated = (active_bug.datecreated - timedelta(days=20)) |
934 | - active_bug.default_bugtask.target.setMaxBugHeat(100) |
935 | - expected = int((fresh_heat * (0.99 ** 20)) + (100 * 0.25 / 20)) |
936 | - heat = BugHeatCalculator(active_bug).getBugHeat() |
937 | - self.assertEqual( |
938 | - expected, heat, |
939 | - "Expected bug heat did not match actual bug heat. " |
940 | - "Expected %s, got %s" % (expected, heat)) |
941 | - |
942 | - |
943 | - |
944 | -def test_suite(): |
945 | - return unittest.TestLoader().loadTestsFromName(__name__) |
946 | |
947 | === modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt' |
948 | --- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-05-27 13:51:06 +0000 |
949 | +++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-14 20:18:33 +0000 |
950 | @@ -1435,7 +1435,7 @@ |
951 | status foo |
952 | ... |
953 | The 'status' command expects any of the following arguments: |
954 | - new, incomplete, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased |
955 | + new, incomplete, opinion, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased |
956 | <BLANKLINE> |
957 | For example: |
958 | <BLANKLINE> |
959 | |
960 | === modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt' |
961 | --- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-04-15 13:26:33 +0000 |
962 | +++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-06-14 20:18:33 +0000 |
963 | @@ -11,6 +11,7 @@ |
964 | ... print status.name |
965 | NEW |
966 | INCOMPLETE |
967 | + OPINION |
968 | INVALID |
969 | WONTFIX |
970 | EXPIRED |
971 | @@ -54,6 +55,7 @@ |
972 | ... print_count_difference(new_bug_counts, old_counts, status) |
973 | NEW: 5 bug(s) more |
974 | INCOMPLETE: 5 bug(s) more |
975 | + OPINION: 5 bug(s) more |
976 | INVALID: 5 bug(s) more |
977 | WONTFIX: 5 bug(s) more |
978 | EXPIRED: 5 bug(s) more |
979 | |
980 | === modified file 'lib/lp/bugs/tests/test_bugheat.py' |
981 | --- lib/lp/bugs/tests/test_bugheat.py 2010-05-27 13:56:03 +0000 |
982 | +++ lib/lp/bugs/tests/test_bugheat.py 2010-06-14 20:18:33 +0000 |
983 | @@ -5,114 +5,13 @@ |
984 | |
985 | __metaclass__ = type |
986 | |
987 | -import pytz |
988 | -import transaction |
989 | import unittest |
990 | -from datetime import datetime |
991 | - |
992 | -from zope.component import getUtility |
993 | - |
994 | -from canonical.launchpad.scripts.tests import run_script |
995 | + |
996 | from canonical.testing import LaunchpadZopelessLayer |
997 | |
998 | -from lp.bugs.adapters.bugchange import BugDescriptionChange |
999 | -from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource |
1000 | -from lp.bugs.model.bugheat import CalculateBugHeatJob |
1001 | -from lp.bugs.scripts.bugheat import BugHeatCalculator |
1002 | -from lp.testing import TestCaseWithFactory |
1003 | from lp.testing.factory import LaunchpadObjectFactory |
1004 | |
1005 | |
1006 | -class CalculateBugHeatJobTestCase(TestCaseWithFactory): |
1007 | - """Test case for CalculateBugHeatJob.""" |
1008 | - |
1009 | - layer = LaunchpadZopelessLayer |
1010 | - |
1011 | - def setUp(self): |
1012 | - super(CalculateBugHeatJobTestCase, self).setUp() |
1013 | - self.bug = self.factory.makeBug() |
1014 | - |
1015 | - # NB: This looks like it should go in the teardown, however |
1016 | - # creating the bug causes a job to be added for it. We clear |
1017 | - # this out so that our tests are consistent. |
1018 | - self._completeJobsAndAssertQueueEmpty() |
1019 | - |
1020 | - def _completeJobsAndAssertQueueEmpty(self): |
1021 | - """Make sure that all the CalculateBugHeatJobs are completed.""" |
1022 | - for bug_job in getUtility(ICalculateBugHeatJobSource).iterReady(): |
1023 | - bug_job.job.start() |
1024 | - bug_job.job.complete() |
1025 | - self.assertEqual(0, self._getJobCount()) |
1026 | - |
1027 | - def _getJobCount(self): |
1028 | - """Return the number of CalculateBugHeatJobs in the queue.""" |
1029 | - return len(self._getJobs()) |
1030 | - |
1031 | - def _getJobs(self): |
1032 | - """Return the pending CalculateBugHeatJobs as a list.""" |
1033 | - return list(CalculateBugHeatJob.iterReady()) |
1034 | - |
1035 | - def test_run(self): |
1036 | - # CalculateBugHeatJob.run() sets calculates and sets the heat |
1037 | - # for a bug. |
1038 | - job = CalculateBugHeatJob.create(self.bug) |
1039 | - bug_heat_calculator = BugHeatCalculator(self.bug) |
1040 | - |
1041 | - job.run() |
1042 | - self.assertEqual( |
1043 | - bug_heat_calculator.getBugHeat(), self.bug.heat) |
1044 | - |
1045 | - def test_utility(self): |
1046 | - # CalculateBugHeatJobSource is a utility for acquiring |
1047 | - # CalculateBugHeatJobs. |
1048 | - utility = getUtility(ICalculateBugHeatJobSource) |
1049 | - self.assertTrue( |
1050 | - ICalculateBugHeatJobSource.providedBy(utility)) |
1051 | - |
1052 | - def test_create_only_creates_one(self): |
1053 | - # If there's already a CalculateBugHeatJob for a bug, |
1054 | - # CalculateBugHeatJob.create() won't create a new one. |
1055 | - job = CalculateBugHeatJob.create(self.bug) |
1056 | - |
1057 | - # There will now be one job in the queue. |
1058 | - self.assertEqual(1, self._getJobCount()) |
1059 | - |
1060 | - new_job = CalculateBugHeatJob.create(self.bug) |
1061 | - |
1062 | - # The two jobs will in fact be the same job. |
1063 | - self.assertEqual(job, new_job) |
1064 | - |
1065 | - # And the queue will still have a length of 1. |
1066 | - self.assertEqual(1, self._getJobCount()) |
1067 | - |
1068 | - def test_cronscript_succeeds(self): |
1069 | - # The calculate-bug-heat cronscript will run all pending |
1070 | - # CalculateBugHeatJobs. |
1071 | - CalculateBugHeatJob.create(self.bug) |
1072 | - transaction.commit() |
1073 | - |
1074 | - retcode, stdout, stderr = run_script( |
1075 | - 'cronscripts/calculate-bug-heat.py', [], |
1076 | - expect_returncode=0) |
1077 | - self.assertEqual('', stdout) |
1078 | - self.assertIn( |
1079 | - 'INFO Ran 1 CalculateBugHeatJob jobs.\n', stderr) |
1080 | - |
1081 | - def test_getOopsVars(self): |
1082 | - # BugJobDerived.getOopsVars() returns the variables to be used |
1083 | - # when logging an OOPS for a bug job. We test this using |
1084 | - # CalculateBugHeatJob because BugJobDerived doesn't let us |
1085 | - # create() jobs. |
1086 | - job = CalculateBugHeatJob.create(self.bug) |
1087 | - vars = job.getOopsVars() |
1088 | - |
1089 | - # The Bug ID, BugJob ID and BugJob type will be returned by |
1090 | - # getOopsVars(). |
1091 | - self.assertIn(('bug_id', self.bug.id), vars) |
1092 | - self.assertIn(('bug_job_id', job.context.id), vars) |
1093 | - self.assertIn(('bug_job_type', job.context.job_type.title), vars) |
1094 | - |
1095 | - |
1096 | class MaxHeatByTargetBase: |
1097 | """Base class for testing a bug target's max_bug_heat attribute.""" |
1098 | |
1099 | |
1100 | === modified file 'lib/lp/code/browser/sourcepackagerecipe.py' |
1101 | --- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-12 13:34:11 +0000 |
1102 | +++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000 |
1103 | @@ -309,7 +309,10 @@ |
1104 | 'name', |
1105 | 'description', |
1106 | 'owner', |
1107 | + 'build_daily' |
1108 | ]) |
1109 | + daily_build_archive = Choice(vocabulary='TargetPPAs', |
1110 | + title=u'Daily build archive') |
1111 | distros = List( |
1112 | Choice(vocabulary='BuildableDistroSeries'), |
1113 | title=u'Default Distribution series') |
1114 | @@ -318,10 +321,16 @@ |
1115 | description=u'The text of the recipe.') |
1116 | |
1117 | |
1118 | + |
1119 | class RecipeTextValidatorMixin: |
1120 | """Class to validate that the Source Package Recipe text is valid.""" |
1121 | |
1122 | def validate(self, data): |
1123 | + if data['build_daily']: |
1124 | + if len(data['distros']) == 0: |
1125 | + self.setFieldError( |
1126 | + 'distros', |
1127 | + 'You must specify at least one series for daily builds.') |
1128 | try: |
1129 | parser = RecipeParser(data['recipe_text']) |
1130 | parser.parse() |
1131 | @@ -343,7 +352,8 @@ |
1132 | def initial_values(self): |
1133 | return { |
1134 | 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity, |
1135 | - 'owner': self.user} |
1136 | + 'owner': self.user, |
1137 | + 'build_daily': False} |
1138 | |
1139 | @property |
1140 | def cancel_url(self): |
1141 | @@ -357,7 +367,8 @@ |
1142 | source_package_recipe = getUtility( |
1143 | ISourcePackageRecipeSource).new( |
1144 | self.user, self.user, data['name'], recipe, |
1145 | - data['description'], data['distros']) |
1146 | + data['description'], data['distros'], |
1147 | + data['daily_build_archive'], data['build_daily']) |
1148 | except ForbiddenInstruction: |
1149 | # XXX: bug=592513 We shouldn't be hardcoding "run" here. |
1150 | self.setFieldError( |
1151 | |
1152 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py' |
1153 | --- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-12 13:52:31 +0000 |
1154 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-14 20:18:33 +0000 |
1155 | @@ -69,6 +69,14 @@ |
1156 | |
1157 | layer = DatabaseFunctionalLayer |
1158 | |
1159 | + def makeBranch(self): |
1160 | + product = self.factory.makeProduct( |
1161 | + name='ratatouille', displayname='Ratatouille') |
1162 | + branch = self.factory.makeBranch( |
1163 | + owner=self.chef, product=product, name='veggies') |
1164 | + self.factory.makeSourcePackage(sourcepackagename='ratatouille') |
1165 | + return branch |
1166 | + |
1167 | def test_create_new_recipe_not_logged_in(self): |
1168 | from canonical.launchpad.testing.pages import setupBrowser |
1169 | product = self.factory.makeProduct( |
1170 | @@ -85,11 +93,7 @@ |
1171 | Unauthorized, browser.getLink('Create packaging recipe').click) |
1172 | |
1173 | def test_create_new_recipe(self): |
1174 | - product = self.factory.makeProduct( |
1175 | - name='ratatouille', displayname='Ratatouille') |
1176 | - branch = self.factory.makeBranch( |
1177 | - owner=self.chef, product=product, name='veggies') |
1178 | - |
1179 | + branch = self.makeBranch() |
1180 | # A new recipe can be created from the branch page. |
1181 | browser = self.getUserBrowser(canonical_url(branch), user=self.chef) |
1182 | browser.getLink('Create packaging recipe').click() |
1183 | @@ -97,6 +101,7 @@ |
1184 | browser.getControl(name='field.name').value = 'daily' |
1185 | browser.getControl('Description').value = 'Make some food!' |
1186 | browser.getControl('Secret Squirrel').click() |
1187 | + browser.getControl('Build daily').click() |
1188 | browser.getControl('Create Recipe').click() |
1189 | |
1190 | pattern = """\ |
1191 | @@ -107,9 +112,11 @@ |
1192 | Make some food! |
1193 | |
1194 | Recipe information |
1195 | + Build daily: True |
1196 | Owner: Master Chef |
1197 | Base branch: lp://dev/~chef/ratatouille/veggies |
1198 | Debian version: 1.0 |
1199 | + Daily build archive: Secret PPA |
1200 | Distribution series: Secret Squirrel |
1201 | .* |
1202 | |
1203 | @@ -168,10 +175,7 @@ |
1204 | def test_create_recipe_bad_text(self): |
1205 | # If a user tries to create source package recipe with bad text, they |
1206 | # should get an error. |
1207 | - product = self.factory.makeProduct( |
1208 | - name='ratatouille', displayname='Ratatouille') |
1209 | - branch = self.factory.makeBranch( |
1210 | - owner=self.chef, product=product, name='veggies') |
1211 | + branch = self.makeBranch() |
1212 | |
1213 | # A new recipe can be created from the branch page. |
1214 | browser = self.getUserBrowser(canonical_url(branch), user=self.chef) |
1215 | @@ -186,6 +190,17 @@ |
1216 | extract_text(find_tags_by_class(browser.contents, 'message')[1]), |
1217 | 'The recipe text is not a valid bzr-builder recipe.') |
1218 | |
1219 | + def test_create_recipe_no_distroseries(self): |
1220 | + browser = self.getViewBrowser(self.makeBranch(), '+new-recipe') |
1221 | + browser.getControl(name='field.name').value = 'daily' |
1222 | + browser.getControl('Description').value = 'Make some food!' |
1223 | + |
1224 | + browser.getControl('Build daily').click() |
1225 | + browser.getControl('Create Recipe').click() |
1226 | + self.assertEqual( |
1227 | + extract_text(find_tags_by_class(browser.contents, 'message')[1]), |
1228 | + 'You must specify at least one series for daily builds.') |
1229 | + |
1230 | def test_create_dupe_recipe(self): |
1231 | # You shouldn't be able to create a duplicate recipe owned by the same |
1232 | # person with the same name. |
1233 | @@ -228,7 +243,11 @@ |
1234 | recipe = self.factory.makeSourcePackageRecipe( |
1235 | owner=self.chef, registrant=self.chef, |
1236 | name=u'things', description=u'This is a recipe', |
1237 | - distroseries=self.squirrel, branches=[veggie_branch]) |
1238 | + distroseries=self.squirrel, branches=[veggie_branch], |
1239 | + daily_build_archive=self.ppa) |
1240 | + self.factory.makeArchive( |
1241 | + distribution=self.ppa.distribution, name='ppa2', |
1242 | + displayname="PPA 2", owner=self.chef) |
1243 | |
1244 | meat_path = meat_branch.bzr_identity |
1245 | |
1246 | @@ -240,6 +259,7 @@ |
1247 | MINIMAL_RECIPE_TEXT % meat_path) |
1248 | browser.getControl('Secret Squirrel').click() |
1249 | browser.getControl('Mumbly Midget').click() |
1250 | + browser.getControl('PPA 2').click() |
1251 | browser.getControl('Update Recipe').click() |
1252 | |
1253 | pattern = """\ |
1254 | @@ -250,9 +270,12 @@ |
1255 | This is stuff |
1256 | |
1257 | Recipe information |
1258 | + Build daily: False |
1259 | Owner: Master Chef |
1260 | Base branch: lp://dev/~chef/ratatouille/meat |
1261 | Debian version: 1.0 |
1262 | + Daily build archive: |
1263 | + PPA 2 |
1264 | Distribution series: Mumbly Midget |
1265 | .* |
1266 | |
1267 | @@ -357,9 +380,13 @@ |
1268 | This is stuff |
1269 | |
1270 | Recipe information |
1271 | + Build daily: |
1272 | + False |
1273 | Owner: Master Chef |
1274 | Base branch: lp://dev/~chef/ratatouille/meat |
1275 | Debian version: 1.0 |
1276 | + Daily build archive: |
1277 | + Secret PPA |
1278 | Distribution series: Mumbly Midget |
1279 | .* |
1280 | |
1281 | @@ -389,9 +416,11 @@ |
1282 | This recipe .*changes. |
1283 | |
1284 | Recipe information |
1285 | + Build daily: False |
1286 | Owner: Master Chef |
1287 | Base branch: lp://dev/~chef/chocolate/cake |
1288 | Debian version: 1.0 |
1289 | + Daily build archive: Secret PPA |
1290 | Distribution series: Secret Squirrel |
1291 | |
1292 | Build records |
1293 | |
1294 | === modified file 'lib/lp/code/configure.zcml' |
1295 | --- lib/lp/code/configure.zcml 2010-06-10 07:55:54 +0000 |
1296 | +++ lib/lp/code/configure.zcml 2010-06-14 20:18:33 +0000 |
1297 | @@ -1052,6 +1052,7 @@ |
1298 | set_attributes=" |
1299 | build_daily |
1300 | builder_recipe |
1301 | + daily_build_archive |
1302 | date_last_modified |
1303 | description |
1304 | distroseries |
1305 | |
1306 | === modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py' |
1307 | --- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-11 05:05:52 +0000 |
1308 | +++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000 |
1309 | @@ -95,7 +95,7 @@ |
1310 | " build a source package for"), |
1311 | readonly=False) |
1312 | build_daily = Bool( |
1313 | - title=_("If true, the recipe should be built daily.")) |
1314 | + title=_("Build daily")) |
1315 | |
1316 | name = exported(TextLine( |
1317 | title=_("Name"), required=True, |
1318 | |
1319 | === modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt' |
1320 | --- lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-04-28 21:18:13 +0000 |
1321 | +++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-06-14 20:18:33 +0000 |
1322 | @@ -36,6 +36,11 @@ |
1323 | <div class="portlet"> |
1324 | <h2>Recipe information</h2> |
1325 | <div class="two-column-list"> |
1326 | + <dl id="build_daily"> |
1327 | + <dt>Build daily:</dt> |
1328 | + <dd tal:content="context/build_daily" /> |
1329 | + </dl> |
1330 | + |
1331 | <dl id="owner"> |
1332 | <dt>Owner:</dt> |
1333 | <dd tal:content="structure context/owner/fmt:link" /> |
1334 | @@ -48,6 +53,14 @@ |
1335 | <dt>Debian version:</dt> |
1336 | <dd tal:content="context/deb_version_template" /> |
1337 | </dl> |
1338 | + <dl id="daily_build_archive"> |
1339 | + <dt>Daily build archive:</dt> |
1340 | + <dd tal:content="structure context/daily_build_archive/fmt:link" |
1341 | + tal:condition="context/daily_build_archive"> |
1342 | + </dd> |
1343 | + <dd tal:condition="not: context/daily_build_archive">None</dd> |
1344 | + </dl> |
1345 | + |
1346 | <dl id="distros"> |
1347 | <dt>Distribution series:</dt> |
1348 | <dd> |
1349 | |
1350 | === modified file 'utilities/report-database-stats.py' |
1351 | --- utilities/report-database-stats.py 2010-04-29 12:38:05 +0000 |
1352 | +++ utilities/report-database-stats.py 2010-06-14 20:18:33 +0000 |
1353 | @@ -72,12 +72,22 @@ |
1354 | |
1355 | |
1356 | def get_cpu_stats(cur, options): |
1357 | + # This query calculates the averate cpu utilization from the |
1358 | + # samples. It assumes samples are taken at regular intervals over |
1359 | + # the period. |
1360 | query = """ |
1361 | - SELECT avg(cpu), username FROM DatabaseCpuStats |
1362 | + SELECT ( |
1363 | + CAST(SUM(cpu) AS float) / ( |
1364 | + SELECT COUNT(DISTINCT date_created) FROM DatabaseCpuStats |
1365 | + WHERE |
1366 | + date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') |
1367 | + - CAST (%s AS interval)) |
1368 | + ) AS avg_cpu, username |
1369 | + FROM DatabaseCpuStats |
1370 | WHERE date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC' |
1371 | - CAST(%s AS interval)) |
1372 | GROUP BY username |
1373 | - """ % sqlvalues(options.since_interval) |
1374 | + """ % sqlvalues(options.since_interval, options.since_interval) |
1375 | |
1376 | cur.execute(query) |
1377 | |
1378 | @@ -107,16 +117,20 @@ |
1379 | tables = get_table_stats(cur, options) |
1380 | arbitrary_table = list(tables)[0] |
1381 | interval = arbitrary_table.date_end - arbitrary_table.date_start |
1382 | - per_minute = interval.days * 24 * 60 + interval.seconds / 60.0 |
1383 | + per_second = float(interval.days * 24 * 60 * 60 + interval.seconds) |
1384 | |
1385 | print "== Most Read Tables ==" |
1386 | |
1387 | + # These match the pg_user_table_stats view. schemaname is the |
1388 | + # namespace (normally 'public'), relname is the table (relation) |
1389 | + # name. total_tup_red is the total number of rows read. |
1390 | + # idx_tup_fetch is the number of rows looked up using an index. |
1391 | tables_sort = ['total_tup_read', 'idx_tup_fetch', 'schemaname', 'relname'] |
1392 | most_read_tables = sorted( |
1393 | tables, key=attrgetter(*tables_sort), reverse=True) |
1394 | for table in most_read_tables[:options.limit]: |
1395 | - print "%40s || %10.2f tuples/min" % ( |
1396 | - table.relname, table.total_tup_read / per_minute) |
1397 | + print "%40s || %10.2f tuples/sec" % ( |
1398 | + table.relname, table.total_tup_read / per_second) |
1399 | |
1400 | |
1401 | print "== Most Written Tables ==" |
1402 | @@ -126,15 +140,15 @@ |
1403 | most_written_tables = sorted( |
1404 | tables, key=attrgetter(*tables_sort), reverse=True) |
1405 | for table in most_written_tables[:options.limit]: |
1406 | - print "%40s || %10.2f tuples/min" % ( |
1407 | - table.relname, table.total_tup_written / per_minute) |
1408 | + print "%40s || %10.2f tuples/sec" % ( |
1409 | + table.relname, table.total_tup_written / per_second) |
1410 | |
1411 | |
1412 | user_cpu = get_cpu_stats(cur, options) |
1413 | print "== Most Active Users ==" |
1414 | |
1415 | for cpu, username in sorted(user_cpu, reverse=True)[:options.limit]: |
1416 | - print "%40s || %6.2f%% CPU" % (username, float(cpu) / 100) |
1417 | + print "%40s || %10.2f%% CPU" % (username, float(cpu) / 10) |
1418 | |
1419 | |
1420 | if __name__ == '__main__': |
Man, these make for better diffs...