Merge lp:~abentley/launchpad/daily-builds-ui into lp:launchpad

Proposed by Aaron Bentley
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
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_recipe_no_distroseries

== 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/request_daily_builds.py
  lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
  lib/lp/code/interfaces/sourcepackagerecipebuild.py
  lib/lp/code/configure.zcml
  lib/canonical/config/schema-lazr.conf
  lib/lp/soyuz/model/publishing.py
  lib/lp/code/scripts/tests/test_request_daily_builds.py
  database/schema/security.cfg
  lib/lp/soyuz/doc/buildd-slavescanner.txt
  lib/lp/soyuz/stories/soyuz/xx-build-record.txt
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/testing/factory.py
  lib/lp/code/model/sourcepackagerecipebuild.py
  lib/lp/code/templates/sourcepackagerecipe-index.pt
  lib/lp/buildmaster/model/buildqueue.py
  lib/lp/soyuz/doc/buildd-dispatching.txt
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  configs/testrunner/launchpad-lazr.conf
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/registry/model/person.py
  lib/lp/soyuz/doc/build.txt

== Pyflakes Doctest notices ==

lib/lp/soyuz/doc/buildd-slavescanner.txt
    689: local variable 'pub_binaries' is assigned to but never used

== Pyflakes notices ==

cronscripts/request_daily_builds.py
    19: 'canonical' imported but unused

    ^^^ fix for circular imports

== Pylint notices ==

cronscripts/request_daily_builds.py
    19: [W0611] Unused import canonical

    ^^^ fix for circular imports

lib/lp/code/model/sourcepackagerecipebuild.py
    207: [W0702, SourcePackageRecipeBuild.makeDailyBuilds] No exception type(s) specified

    ^^^ expected; this is a generic error handler.

lib/lp/code/interfaces/sourcepackagerecipe.py
    150: [C0322, ISourcePackageRecipe.requestBuild] Operator not preceded by a space
    distroseries=Reference(schema=IDistroSeries),
    ^
    )
    @export_write_operation()
    def requestBuild(archive, distroseries, requester, pocket):

    ^^^ bogus

lib/lp/registry/model/person.py
    1265: [W0104, Person.addMember] Statement seems to have no effect

    ^^^ read to force a flush.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

Man, these make for better diffs...

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2010-06-11 18:57:02 +0000
+++ Makefile 2010-06-14 20:18:33 +0000
@@ -250,7 +250,7 @@
250 bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG)250 bin/run -r librarian,sftp,codebrowse -i $(LPCONFIG)
251251
252252
253start_librarian: build253start_librarian: compile
254 bin/start_librarian254 bin/start_librarian
255255
256stop_librarian:256stop_librarian:
257257
=== removed file 'cronscripts/calculate-bug-heat.py'
--- cronscripts/calculate-bug-heat.py 2010-04-27 19:48:39 +0000
+++ cronscripts/calculate-bug-heat.py 1970-01-01 00:00:00 +0000
@@ -1,33 +0,0 @@
1#!/usr/bin/python -S
2#
3# Copyright 2010 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6# pylint: disable-msg=W0403
7
8"""Calculate bug heat."""
9
10__metaclass__ = type
11
12import _pythonpath
13
14from canonical.launchpad.webapp import errorlog
15
16from lp.services.job.runner import JobCronScript
17from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
18
19
20class RunCalculateBugHeat(JobCronScript):
21 """Run BranchScanJob jobs."""
22
23 config_name = 'calculate_bug_heat'
24 source_interface = ICalculateBugHeatJobSource
25
26 def main(self):
27 errorlog.globalErrorUtility.configure(self.config_name)
28 return super(RunCalculateBugHeat, self).main()
29
30
31if __name__ == '__main__':
32 script = RunCalculateBugHeat()
33 script.lock_and_run()
340
=== modified file 'cronscripts/publishing/maintenance-check.py'
--- cronscripts/publishing/maintenance-check.py 2010-04-23 13:43:19 +0000
+++ cronscripts/publishing/maintenance-check.py 2010-06-14 20:18:33 +0000
@@ -350,7 +350,7 @@
350 except:350 except:
351 logging.exception("can not parse line '%s'" % line)351 logging.exception("can not parse line '%s'" % line)
352 except urllib2.HTTPError, e:352 except urllib2.HTTPError, e:
353 if e.getcode() != 404:353 if e.code != 404:
354 raise354 raise
355 sys.stderr.write("hints-file: %s gave 404 error\n" % hints_file)355 sys.stderr.write("hints-file: %s gave 404 error\n" % hints_file)
356 356
357357
=== modified file 'database/replication/helpers.py'
--- database/replication/helpers.py 2010-04-29 12:38:05 +0000
+++ database/replication/helpers.py 2010-06-14 20:18:33 +0000
@@ -44,7 +44,6 @@
44 ('public', 'nameblacklist'),44 ('public', 'nameblacklist'),
45 ('public', 'openidconsumerassociation'),45 ('public', 'openidconsumerassociation'),
46 ('public', 'openidconsumernonce'),46 ('public', 'openidconsumernonce'),
47 ('public', 'oauthnonce'),
48 ('public', 'codeimportmachine'),47 ('public', 'codeimportmachine'),
49 ('public', 'scriptactivity'),48 ('public', 'scriptactivity'),
50 ('public', 'standardshipitrequest'),49 ('public', 'standardshipitrequest'),
@@ -71,6 +70,8 @@
71 # Database statistics70 # Database statistics
72 'public.databasetablestats',71 'public.databasetablestats',
73 'public.databasecpustats',72 'public.databasecpustats',
73 # Don't replicate OAuthNonce - too busy and no real gain.
74 'public.oauthnonce',
74 # Ubuntu SSO database. These tables where created manually by ISD75 # Ubuntu SSO database. These tables where created manually by ISD
75 # and the Launchpad scripts should not mess with them. Eventually76 # and the Launchpad scripts should not mess with them. Eventually
76 # these tables will be in a totally separate database.77 # these tables will be in a totally separate database.
@@ -353,6 +354,9 @@
353354
354 A replication set must contain all tables linked by foreign key355 A replication set must contain all tables linked by foreign key
355 reference to the given table, and sequences used to generate keys.356 reference to the given table, and sequences used to generate keys.
357 Tables and sequences can be added to the IGNORED_TABLES and
358 IGNORED_SEQUENCES lists for cases where we known can safely ignore
359 this restriction.
356360
357 :param seeds: [(namespace, tablename), ...]361 :param seeds: [(namespace, tablename), ...]
358362
@@ -420,7 +424,8 @@
420 """ % sqlvalues(namespace, tablename))424 """ % sqlvalues(namespace, tablename))
421 for namespace, tablename in cur.fetchall():425 for namespace, tablename in cur.fetchall():
422 key = (namespace, tablename)426 key = (namespace, tablename)
423 if key not in tables and key not in pending_tables:427 if (key not in tables and key not in pending_tables
428 and '%s.%s' % (namespace, tablename) not in IGNORED_TABLES):
424 pending_tables.add(key)429 pending_tables.add(key)
425430
426 # Generate the set of sequences that are linked to any of our set of431 # Generate the set of sequences that are linked to any of our set of
@@ -441,8 +446,9 @@
441 ) AS whatever446 ) AS whatever
442 WHERE seq IS NOT NULL;447 WHERE seq IS NOT NULL;
443 """ % sqlvalues(fqn(namespace, tablename), namespace, tablename))448 """ % sqlvalues(fqn(namespace, tablename), namespace, tablename))
444 for row in cur.fetchall():449 for sequence, in cur.fetchall():
445 sequences.add(row[0])450 if sequence not in IGNORED_SEQUENCES:
451 sequences.add(sequence)
446452
447 # We can't easily convert the sequence name to (namespace, name) tuples,453 # We can't easily convert the sequence name to (namespace, name) tuples,
448 # so we might as well convert the tables to dot notation for consistancy.454 # so we might as well convert the tables to dot notation for consistancy.
449455
=== modified file 'database/replication/new-slave.py'
--- database/replication/new-slave.py 2010-05-19 18:07:56 +0000
+++ database/replication/new-slave.py 2010-06-14 20:18:33 +0000
@@ -188,6 +188,9 @@
188188
189 script += dedent("""\189 script += dedent("""\
190 } on error { echo 'Failed.'; exit 1; }190 } on error { echo 'Failed.'; exit 1; }
191
192 echo 'You may need to restart the Slony daemons now. If the first';
193 echo 'of the following syncs passes then there is no need.';
191 """)194 """)
192195
193 full_sync = []196 full_sync = []
@@ -200,6 +203,7 @@
200 wait for event (203 wait for event (
201 origin = @%(nickname)s, confirmed=ALL,204 origin = @%(nickname)s, confirmed=ALL,
202 wait on = @%(nickname)s, timeout=0);205 wait on = @%(nickname)s, timeout=0);
206 echo 'Ok. Replication syncing fine with new node.';
203 """ % {'nickname': nickname}))207 """ % {'nickname': nickname}))
204 full_sync = '\n'.join(full_sync)208 full_sync = '\n'.join(full_sync)
205 script += full_sync209 script += full_sync
@@ -210,6 +214,7 @@
210 subscribe set (214 subscribe set (
211 id=%d, provider=@master_node, receiver=@new_node, forward=yes);215 id=%d, provider=@master_node, receiver=@new_node, forward=yes);
212 echo 'Waiting for subscribe to start processing.';216 echo 'Waiting for subscribe to start processing.';
217 echo 'This will block on long running transactions.';
213 sync (id = @master_node);218 sync (id = @master_node);
214 wait for event (219 wait for event (
215 origin = @master_node, confirmed = ALL,220 origin = @master_node, confirmed = ALL,
216221
=== modified file 'database/schema/comments.sql'
--- database/schema/comments.sql 2010-05-27 22:18:16 +0000
+++ database/schema/comments.sql 2010-06-14 20:18:33 +0000
@@ -1350,6 +1350,7 @@
1350COMMENT ON COLUMN SourcePackageRecipe.owner IS 'The person or team who can edit this recipe.';1350COMMENT ON COLUMN SourcePackageRecipe.owner IS 'The person or team who can edit this recipe.';
1351COMMENT ON COLUMN SourcePackageRecipe.name IS 'The name of the recipe in the web/URL.';1351COMMENT ON COLUMN SourcePackageRecipe.name IS 'The name of the recipe in the web/URL.';
1352COMMENT ON COLUMN SourcePackageRecipe.build_daily IS 'If true, this recipe should be built daily.';1352COMMENT ON COLUMN SourcePackageRecipe.build_daily IS 'If true, this recipe should be built daily.';
1353COMMENT ON COLUMN SourcePackageRecipe.is_stale IS 'True if this recipe has not been built since a branch was updated.';
13531354
1354COMMENT ON COLUMN SourcePackageREcipe.daily_build_archive IS 'The archive to build into for daily builds.';1355COMMENT ON COLUMN SourcePackageREcipe.daily_build_archive IS 'The archive to build into for daily builds.';
13551356
@@ -1371,6 +1372,7 @@
1371COMMENT 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.';1372COMMENT 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.';
1372COMMENT ON COLUMN SourcePackageRecipeBuild.requester IS 'Who requested the build.';1373COMMENT ON COLUMN SourcePackageRecipeBuild.requester IS 'Who requested the build.';
1373COMMENT ON COLUMN SourcePackageRecipeBuild.recipe IS 'The recipe being processed.';1374COMMENT ON COLUMN SourcePackageRecipeBuild.recipe IS 'The recipe being processed.';
1375COMMENT ON COLUMN SourcePackageRecipeBuild.manifest IS 'The evaluated recipe that was built.';
1374COMMENT ON COLUMN SourcePackageRecipeBuild.archive IS 'The archive the source package will be built in and uploaded to.';1376COMMENT ON COLUMN SourcePackageRecipeBuild.archive IS 'The archive the source package will be built in and uploaded to.';
1375COMMENT ON COLUMN SourcePackageRecipeBuild.pocket IS 'The pocket the source package will be built in and uploaded to.';1377COMMENT ON COLUMN SourcePackageRecipeBuild.pocket IS 'The pocket the source package will be built in and uploaded to.';
1376COMMENT ON COLUMN SourcePackageRecipeBuild.dependencies IS 'The missing build dependencies, if any.';1378COMMENT ON COLUMN SourcePackageRecipeBuild.dependencies IS 'The missing build dependencies, if any.';
13771379
=== modified file 'database/schema/fti.py'
--- database/schema/fti.py 2010-05-19 18:07:56 +0000
+++ database/schema/fti.py 2010-06-14 20:18:33 +0000
@@ -14,10 +14,10 @@
14import _pythonpath14import _pythonpath
1515
16from distutils.version import LooseVersion16from distutils.version import LooseVersion
17import sys
18import os.path17import os.path
19from optparse import OptionParser18from optparse import OptionParser
20import popen219import subprocess
20import sys
21from tempfile import NamedTemporaryFile21from tempfile import NamedTemporaryFile
22from textwrap import dedent22from textwrap import dedent
23import time23import time
@@ -319,18 +319,15 @@
319 cmd += ' -h %s' % lp.dbhost319 cmd += ' -h %s' % lp.dbhost
320 if options.dbuser:320 if options.dbuser:
321 cmd += ' -U %s' % options.dbuser321 cmd += ' -U %s' % options.dbuser
322 p = popen2.Popen4(cmd)322 p = subprocess.Popen(
323 c = p.tochild323 cmd.split(' '), stdin=subprocess.PIPE,
324 print >> c, "SET client_min_messages=ERROR;"324 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
325 print >> c, "CREATE SCHEMA ts2;"325 out, err = p.communicate(
326 print >> c, open(tsearch2_sql_path).read().replace(326 "SET client_min_messages=ERROR; CREATE SCHEMA ts2;"
327 'public;','ts2, public;'327 + open(tsearch2_sql_path).read().replace('public;','ts2, public;'))
328 )328 if p.returncode != 0:
329 p.tochild.close()
330 rv = p.wait()
331 if rv != 0:
332 log.fatal('Error executing %s:', cmd)329 log.fatal('Error executing %s:', cmd)
333 log.debug(p.fromchild.read())330 log.debug(out)
334 sys.exit(rv)331 sys.exit(rv)
335332
336 # Create ftq helper and its sibling _ftq.333 # Create ftq helper and its sibling _ftq.
337334
=== added file 'database/schema/patch-2207-60-1.sql'
--- database/schema/patch-2207-60-1.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2207-60-1.sql 2010-06-14 20:18:33 +0000
@@ -0,0 +1,10 @@
1SET client_min_messages=ERROR;
2
3CREATE INDEX archive__require_virtualized__idx
4ON Archive(require_virtualized);
5
6CREATE INDEX buildfarmjob__status__idx
7ON BuildFarmJob(status);
8
9INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 60, 1);
10
011
=== added file 'database/schema/patch-2207-61-0.sql'
--- database/schema/patch-2207-61-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2207-61-0.sql 2010-06-14 20:18:33 +0000
@@ -0,0 +1,13 @@
1-- Copyright 2010 Canonical Ltd. This software is licensed under the
2-- GNU Affero General Public License version 3 (see the file LICENSE).
3
4SET client_min_messages=ERROR;
5ALTER TABLE SourcePackageRecipe ADD COLUMN is_stale BOOLEAN NOT NULL DEFAULT TRUE;
6ALTER TABLE SourcePackageRecipeBuild ADD COLUMN manifest INTEGER REFERENCES SourcePackageRecipeData;
7
8CREATE INDEX sourcepackagerecipe__is_stale__build_daily__idx
9ON SourcepackageRecipe(is_stale, build_daily);
10
11CREATE INDEX sourcepackagerecipebuild__manifest__idx ON SourcepackageRecipeBuild(manifest);
12
13INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 61, 0);
014
=== added file 'database/schema/patch-2207-62-0.sql'
--- database/schema/patch-2207-62-0.sql 1970-01-01 00:00:00 +0000
+++ database/schema/patch-2207-62-0.sql 2010-06-14 20:18:33 +0000
@@ -0,0 +1,14 @@
1SET client_min_messages=ERROR;
2
3-- Bug #49717
4ALTER TABLE SourcePackageRelease ALTER component SET NOT NULL;
5
6-- We are taking OAuthNonce out of replication, so we make the foreign
7-- key reference ON DELETE CASCADE so things don't explode when we
8-- shuffle the lpmain master around.
9ALTER TABLE OAuthNonce DROP CONSTRAINT oauthnonce__access_token__fk;
10ALTER TABLE OAuthNonce ADD CONSTRAINT oauthnonce__access_token__fk
11 FOREIGN KEY (access_token) REFERENCES OAuthAccessToken
12 ON DELETE CASCADE;
13
14INSERT INTO LaunchpadDatabaseRevision VALUES (2207, 62, 0);
015
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2010-06-08 15:13:20 +0000
+++ database/schema/security.cfg 2010-06-14 20:18:33 +0000
@@ -1847,6 +1847,7 @@
1847type=user1847type=user
1848public.archive = SELECT1848public.archive = SELECT
1849public.buildfarmjob = SELECT1849public.buildfarmjob = SELECT
1850public.databasereplicationlag = SELECT
1850public.packagebuild = SELECT1851public.packagebuild = SELECT
1851public.binarypackagebuild = SELECT1852public.binarypackagebuild = SELECT
1852public.buildqueue = SELECT1853public.buildqueue = SELECT
18531854
=== modified file 'database/schema/trusted.sql'
--- database/schema/trusted.sql 2010-05-28 10:36:08 +0000
+++ database/schema/trusted.sql 2010-06-14 20:18:33 +0000
@@ -144,6 +144,12 @@
144 LIMIT 1144 LIMIT 1
145 """, 1).nrows() > 0145 """, 1).nrows() > 0
146 if stats_reset:146 if stats_reset:
147 # The database stats have been reset. We cannot calculate
148 # deltas because we do not know when this happened. So we trash
149 # our records as they are now useless to us. We could be more
150 # sophisticated about this, but this should only happen
151 # when an admin explicitly resets the statistics or if the
152 # database is rebuilt.
147 plpy.notice("Stats wraparound. Purging DatabaseTableStats")153 plpy.notice("Stats wraparound. Purging DatabaseTableStats")
148 plpy.execute("DELETE FROM DatabaseTableStats")154 plpy.execute("DELETE FROM DatabaseTableStats")
149 else:155 else:
@@ -158,7 +164,8 @@
158 SELECT164 SELECT
159 CURRENT_TIMESTAMP AT TIME ZONE 'UTC',165 CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
160 schemaname, relname, seq_scan, seq_tup_read,166 schemaname, relname, seq_scan, seq_tup_read,
161 idx_scan, idx_tup_fetch, n_tup_ins, n_tup_upd, n_tup_del,167 coalesce(idx_scan, 0), coalesce(idx_tup_fetch, 0),
168 n_tup_ins, n_tup_upd, n_tup_del,
162 n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum,169 n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum,
163 last_autovacuum, last_analyze, last_autoanalyze170 last_autovacuum, last_analyze, last_autoanalyze
164 FROM pg_catalog.pg_stat_user_tables;171 FROM pg_catalog.pg_stat_user_tables;
165172
=== modified file 'lib/canonical/launchpad/scripts/garbo.py'
--- lib/canonical/launchpad/scripts/garbo.py 2010-06-11 07:26:03 +0000
+++ lib/canonical/launchpad/scripts/garbo.py 2010-06-14 20:18:33 +0000
@@ -33,7 +33,6 @@
33from lp.bugs.interfaces.bug import IBugSet33from lp.bugs.interfaces.bug import IBugSet
34from lp.bugs.model.bug import Bug34from lp.bugs.model.bug import Bug
35from lp.bugs.model.bugattachment import BugAttachment35from lp.bugs.model.bugattachment import BugAttachment
36from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
37from lp.bugs.model.bugnotification import BugNotification36from lp.bugs.model.bugnotification import BugNotification
38from lp.bugs.model.bugwatch import BugWatch37from lp.bugs.model.bugwatch import BugWatch
39from lp.bugs.scripts.checkwatches.scheduler import (38from lp.bugs.scripts.checkwatches.scheduler import (
4039
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2010-05-25 16:45:26 +0000
+++ lib/lp/bugs/browser/bugtask.py 2010-06-14 20:18:33 +0000
@@ -2646,7 +2646,7 @@
2646 dict(2646 dict(
2647 value=term.token, title=term.title or term.token,2647 value=term.token, title=term.title or term.token,
2648 checked=term.value in default_values))2648 checked=term.value in default_values))
2649 return helpers.shortlist(widget_values, longest_expected=11)2649 return helpers.shortlist(widget_values, longest_expected=12)
26502650
2651 def getStatusWidgetValues(self):2651 def getStatusWidgetValues(self):
2652 """Return data used to render the status checkboxes."""2652 """Return data used to render the status checkboxes."""
26532653
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2010-05-25 14:50:42 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2010-06-14 20:18:33 +0000
@@ -245,8 +245,8 @@
245 self.bug.default_bugtask, LaunchpadTestRequest())245 self.bug.default_bugtask, LaunchpadTestRequest())
246 view.initialize()246 view.initialize()
247 self.assertEqual(247 self.assertEqual(
248 ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',248 ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
249 'Fix Committed', 'Fix Released'],249 'In Progress', 'Fix Committed', 'Fix Released'],
250 self.getWidgetOptionTitles(view.form_fields['status']))250 self.getWidgetOptionTitles(view.form_fields['status']))
251251
252 def test_status_field_privileged_persons(self):252 def test_status_field_privileged_persons(self):
@@ -260,8 +260,9 @@
260 self.bug.default_bugtask, LaunchpadTestRequest())260 self.bug.default_bugtask, LaunchpadTestRequest())
261 view.initialize()261 view.initialize()
262 self.assertEqual(262 self.assertEqual(
263 ['New', 'Incomplete', 'Invalid', "Won't Fix", 'Confirmed',263 ['New', 'Incomplete', 'Opinion', 'Invalid', "Won't Fix",
264 'Triaged', 'In Progress', 'Fix Committed', 'Fix Released'],264 'Confirmed', 'Triaged', 'In Progress', 'Fix Committed',
265 'Fix Released'],
265 self.getWidgetOptionTitles(view.form_fields['status']),266 self.getWidgetOptionTitles(view.form_fields['status']),
266 'Unexpected set of settable status options for %s'267 'Unexpected set of settable status options for %s'
267 % user.name)268 % user.name)
@@ -278,8 +279,8 @@
278 self.bug.default_bugtask, LaunchpadTestRequest())279 self.bug.default_bugtask, LaunchpadTestRequest())
279 view.initialize()280 view.initialize()
280 self.assertEqual(281 self.assertEqual(
281 ['New', 'Incomplete', 'Invalid', 'Confirmed', 'In Progress',282 ['New', 'Incomplete', 'Opinion', 'Invalid', 'Confirmed',
282 'Fix Committed', 'Fix Released', 'Unknown'],283 'In Progress', 'Fix Committed', 'Fix Released', 'Unknown'],
283 self.getWidgetOptionTitles(view.form_fields['status']))284 self.getWidgetOptionTitles(view.form_fields['status']))
284285
285 def test_status_field_bug_task_in_status_expired(self):286 def test_status_field_bug_task_in_status_expired(self):
@@ -292,8 +293,8 @@
292 self.bug.default_bugtask, LaunchpadTestRequest())293 self.bug.default_bugtask, LaunchpadTestRequest())
293 view.initialize()294 view.initialize()
294 self.assertEqual(295 self.assertEqual(
295 ['New', 'Incomplete', 'Invalid', 'Expired', 'Confirmed',296 ['New', 'Incomplete', 'Opinion', 'Invalid', 'Expired',
296 'In Progress', 'Fix Committed', 'Fix Released'],297 'Confirmed', 'In Progress', 'Fix Committed', 'Fix Released'],
297 self.getWidgetOptionTitles(view.form_fields['status']))298 self.getWidgetOptionTitles(view.form_fields['status']))
298299
299300
300301
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-06-04 09:31:21 +0000
+++ lib/lp/bugs/configure.zcml 2010-06-14 20:18:33 +0000
@@ -969,18 +969,6 @@
969 factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb"969 factory="lp.bugs.browser.bugtarget.BugsVHostBreadcrumb"
970 permission="zope.Public"/>970 permission="zope.Public"/>
971971
972 <!-- CalculateBugHeatJobs -->
973 <class class="lp.bugs.model.bugheat.CalculateBugHeatJob">
974 <allow interface="lp.bugs.interfaces.bugjob.IBugJob" />
975 <allow interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJob"/>
976 </class>
977 <securedutility
978 component="lp.bugs.model.bugheat.CalculateBugHeatJob"
979 provides="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource">
980 <allow
981 interface="lp.bugs.interfaces.bugjob.ICalculateBugHeatJobSource"/>
982 </securedutility>
983
984 <!-- ProcessApportBlobJobs -->972 <!-- ProcessApportBlobJobs -->
985 <class class="lp.bugs.model.apportjob.ProcessApportBlobJob">973 <class class="lp.bugs.model.apportjob.ProcessApportBlobJob">
986 <allow interface="lp.bugs.interfaces.apportjob.IApportJob" />974 <allow interface="lp.bugs.interfaces.apportjob.IApportJob" />
987975
=== modified file 'lib/lp/bugs/doc/bugtask-status-workflow.txt'
--- lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-04-15 15:28:22 +0000
+++ lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-06-14 20:18:33 +0000
@@ -145,7 +145,7 @@
145 >>> ubuntu_firefox_task.date_inprogress is None145 >>> ubuntu_firefox_task.date_inprogress is None
146 True146 True
147147
148Marking the bug Triaged sets `date_triged`.148Marking the bug Triaged sets `date_triaged`.
149149
150 >>> print ubuntu_firefox_task.date_triaged150 >>> print ubuntu_firefox_task.date_triaged
151 None151 None
@@ -188,6 +188,16 @@
188188
189 >>> ubuntu_firefox_task.transitionToStatus(189 >>> ubuntu_firefox_task.transitionToStatus(
190 ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)190 ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
191 >>> ubuntu_firefox_task.date_closed is None
192 True
193
194 >>> ubuntu_firefox_task.transitionToStatus(
195 ... BugTaskStatus.OPINION, getUtility(ILaunchBag).user)
196 >>> ubuntu_firefox_task.date_closed
197 datetime.datetime...
198
199 >>> ubuntu_firefox_task.transitionToStatus(
200 ... BugTaskStatus.CONFIRMED, getUtility(ILaunchBag).user)
191 >>> ubuntu_firefox_task.date_inprogress is None201 >>> ubuntu_firefox_task.date_inprogress is None
192 True202 True
193 >>> ubuntu_firefox_task.transitionToStatus(203 >>> ubuntu_firefox_task.transitionToStatus(
194204
=== modified file 'lib/lp/bugs/interfaces/bugjob.py'
--- lib/lp/bugs/interfaces/bugjob.py 2010-01-22 21:44:19 +0000
+++ lib/lp/bugs/interfaces/bugjob.py 2010-06-14 20:18:33 +0000
@@ -8,8 +8,6 @@
8 'BugJobType',8 'BugJobType',
9 'IBugJob',9 'IBugJob',
10 'IBugJobSource',10 'IBugJobSource',
11 'ICalculateBugHeatJob',
12 'ICalculateBugHeatJobSource',
13 ]11 ]
1412
15from zope.interface import Attribute, Interface13from zope.interface import Attribute, Interface
@@ -19,7 +17,7 @@
1917
20from lazr.enum import DBEnumeratedType, DBItem18from lazr.enum import DBEnumeratedType, DBItem
21from lp.bugs.interfaces.bug import IBug19from lp.bugs.interfaces.bug import IBug
22from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob20from lp.services.job.interfaces.job import IJob, IJobSource
2321
2422
25class BugJobType(DBEnumeratedType):23class BugJobType(DBEnumeratedType):
@@ -57,11 +55,3 @@
5755
58 def create(bug):56 def create(bug):
59 """Create a new IBugJob for a bug."""57 """Create a new IBugJob for a bug."""
60
61
62class ICalculateBugHeatJob(IRunnableJob):
63 """A Job to calculate bug heat."""
64
65
66class ICalculateBugHeatJobSource(IBugJobSource):
67 """Interface for acquiring CalculateBugHeatJobs."""
6858
=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
--- lib/lp/bugs/interfaces/bugtask.py 2010-06-07 19:48:29 +0000
+++ lib/lp/bugs/interfaces/bugtask.py 2010-06-14 20:18:33 +0000
@@ -159,6 +159,14 @@
159 the user was visiting when the bug occurred, etc.159 the user was visiting when the bug occurred, etc.
160 """)160 """)
161161
162 OPINION = DBItem(16, """
163 Opinion
164
165 The bug remains open for discussion only. This status is usually
166 used where there is disagreement over whether the bug is relevant
167 to the current target and whether it should be fixed.
168 """)
169
162 INVALID = DBItem(17, """170 INVALID = DBItem(17, """
163 Invalid171 Invalid
164172
@@ -235,8 +243,8 @@
235243
236 sort_order = (244 sort_order = (
237 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',245 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',
238 'INCOMPLETE', 'INVALID', 'WONTFIX', 'EXPIRED', 'CONFIRMED', 'TRIAGED',246 'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED',
239 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')247 'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
240248
241 INCOMPLETE_WITH_RESPONSE = DBItem(35, """249 INCOMPLETE_WITH_RESPONSE = DBItem(35, """
242 Incomplete (with response)250 Incomplete (with response)
@@ -312,6 +320,7 @@
312320
313RESOLVED_BUGTASK_STATUSES = (321RESOLVED_BUGTASK_STATUSES = (
314 BugTaskStatus.FIXRELEASED,322 BugTaskStatus.FIXRELEASED,
323 BugTaskStatus.OPINION,
315 BugTaskStatus.INVALID,324 BugTaskStatus.INVALID,
316 BugTaskStatus.WONTFIX,325 BugTaskStatus.WONTFIX,
317 BugTaskStatus.EXPIRED)326 BugTaskStatus.EXPIRED)
318327
=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py 2010-06-10 18:55:22 +0000
+++ lib/lp/bugs/model/bug.py 2010-06-14 20:18:33 +0000
@@ -83,7 +83,6 @@
83from lp.bugs.interfaces.bugtracker import BugTrackerType83from lp.bugs.interfaces.bugtracker import BugTrackerType
84from lp.bugs.interfaces.bugwatch import IBugWatchSet84from lp.bugs.interfaces.bugwatch import IBugWatchSet
85from lp.bugs.interfaces.cve import ICveSet85from lp.bugs.interfaces.cve import ICveSet
86from lp.bugs.scripts.bugheat import BugHeatConstants
87from lp.bugs.model.bugattachment import BugAttachment86from lp.bugs.model.bugattachment import BugAttachment
88from lp.bugs.model.bugbranch import BugBranch87from lp.bugs.model.bugbranch import BugBranch
89from lp.bugs.model.bugcve import BugCve88from lp.bugs.model.bugcve import BugCve
9089
=== removed file 'lib/lp/bugs/model/bugheat.py'
--- lib/lp/bugs/model/bugheat.py 2010-01-21 20:46:03 +0000
+++ lib/lp/bugs/model/bugheat.py 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Job classes related to BugJobs are in here."""
5
6__metaclass__ = type
7__all__ = [
8 'CalculateBugHeatJob',
9 ]
10
11from zope.component import getUtility
12from zope.interface import classProvides, implements
13
14from canonical.launchpad.webapp.interfaces import (
15 DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
16
17from lp.bugs.interfaces.bugjob import (
18 BugJobType, ICalculateBugHeatJob, ICalculateBugHeatJobSource)
19from lp.bugs.model.bugjob import BugJob, BugJobDerived
20from lp.bugs.scripts.bugheat import BugHeatCalculator
21from lp.services.job.model.job import Job
22
23
24class CalculateBugHeatJob(BugJobDerived):
25 """A Job to calculate bug heat."""
26 implements(ICalculateBugHeatJob)
27
28 class_job_type = BugJobType.UPDATE_HEAT
29 classProvides(ICalculateBugHeatJobSource)
30
31 def run(self):
32 """See `IRunnableJob`."""
33 calculator = BugHeatCalculator(self.bug)
34 calculated_heat = calculator.getBugHeat()
35 self.bug.setHeat(calculated_heat)
36
37 @classmethod
38 def create(cls, bug):
39 """See `ICalculateBugHeatJobSource`."""
40 # If there's already a job for the bug, don't create a new one.
41 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
42 job_for_bug = store.find(
43 BugJob,
44 BugJob.bug == bug,
45 BugJob.job_type == cls.class_job_type,
46 BugJob.job == Job.id,
47 Job.id.is_in(Job.ready_jobs)
48 ).any()
49
50 if job_for_bug is not None:
51 return cls(job_for_bug)
52 else:
53 return super(CalculateBugHeatJob, cls).create(bug)
54
550
=== removed file 'lib/lp/bugs/scripts/bugheat.py'
--- lib/lp/bugs/scripts/bugheat.py 2010-04-29 11:31:49 +0000
+++ lib/lp/bugs/scripts/bugheat.py 1970-01-01 00:00:00 +0000
@@ -1,108 +0,0 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""The innards of the Bug Heat cronscript."""
5
6__metaclass__ = type
7__all__ = [
8 'BugHeatCalculator',
9 'BugHeatConstants',
10 ]
11
12from datetime import datetime
13
14from lp.bugs.interfaces.bugtask import RESOLVED_BUGTASK_STATUSES
15
16class BugHeatConstants:
17
18 PRIVACY = 150
19 SECURITY = 250
20 DUPLICATE = 6
21 AFFECTED_USER = 4
22 SUBSCRIBER = 2
23
24
25class BugHeatCalculator:
26 """A class to calculate the heat for a bug."""
27 # If you change the way that bug heat is calculated, remember to update
28 # the description of how it is calculated at
29 # /lib/lp/bugs/help/bug-heat.html and
30 # https://help.launchpad.net/Bugs/BugHeat
31
32 def __init__(self, bug):
33 self.bug = bug
34
35 def _getHeatFromPrivacy(self):
36 """Return the heat generated by the bug's `private` attribute."""
37 if self.bug.private:
38 return BugHeatConstants.PRIVACY
39 else:
40 return 0
41
42 def _getHeatFromSecurity(self):
43 """Return the heat generated if the bug is security related."""
44 if self.bug.security_related:
45 return BugHeatConstants.SECURITY
46 else:
47 return 0
48
49 def _getHeatFromDuplicates(self):
50 """Return the heat generated by the bug's duplicates."""
51 return self.bug.duplicates.count() * BugHeatConstants.DUPLICATE
52
53 def _getHeatFromAffectedUsers(self):
54 """Return the heat generated by the bug's affected users."""
55 return (
56 self.bug.users_affected_count_with_dupes *
57 BugHeatConstants.AFFECTED_USER)
58
59 def _getHeatFromSubscribers(self):
60 """Return the heat generated by the bug's subscribers."""
61 direct_subscribers = self.bug.getDirectSubscribers()
62 subscribers_from_dupes = self.bug.getSubscribersFromDuplicates()
63
64 subscriber_count = (
65 len(direct_subscribers) + len(subscribers_from_dupes))
66 return subscriber_count * BugHeatConstants.SUBSCRIBER
67
68 def _bugIsComplete(self):
69 """Are all the tasks for this bug resolved?"""
70 return all([(task.status in RESOLVED_BUGTASK_STATUSES)
71 for task in self.bug.bugtasks])
72
73 def getBugHeat(self):
74 """Return the total heat for the current bug."""
75 if self._bugIsComplete():
76 return 0
77
78 total_heat = sum([
79 self._getHeatFromAffectedUsers(),
80 self._getHeatFromDuplicates(),
81 self._getHeatFromPrivacy(),
82 self._getHeatFromSecurity(),
83 self._getHeatFromSubscribers(),
84 ])
85
86 # Bugs decay over time. Every day the bug isn't touched its heat
87 # decreases by 1%.
88 days = (
89 datetime.utcnow() -
90 self.bug.date_last_updated.replace(tzinfo=None)).days
91 total_heat = int(total_heat * (0.99 ** days))
92
93 if days > 0:
94 # Bug heat increases by a quarter of the maximum bug heat divided
95 # by the number of days since the bug's creation date.
96 days_since_last_activity = (
97 datetime.utcnow() -
98 max(self.bug.date_last_updated.replace(tzinfo=None),
99 self.bug.date_last_message.replace(tzinfo=None))).days
100 days_since_created = (
101 datetime.utcnow() - self.bug.datecreated.replace(tzinfo=None)).days
102 max_heat = max(
103 task.target.max_bug_heat for task in self.bug.bugtasks)
104 if max_heat is not None and days_since_created > 0:
105 total_heat = total_heat + (max_heat * 0.25 / days_since_created)
106
107 return int(total_heat)
108
1090
=== removed file 'lib/lp/bugs/scripts/tests/test_bugheat.py'
--- lib/lp/bugs/scripts/tests/test_bugheat.py 2010-04-29 11:31:49 +0000
+++ lib/lp/bugs/scripts/tests/test_bugheat.py 1970-01-01 00:00:00 +0000
@@ -1,256 +0,0 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Module docstring goes here."""
5
6__metaclass__ = type
7
8import unittest
9
10from datetime import datetime, timedelta
11
12from canonical.testing import LaunchpadZopelessLayer
13
14from lp.bugs.interfaces.bugtask import BugTaskStatus
15from lp.bugs.scripts.bugheat import BugHeatCalculator, BugHeatConstants
16from lp.testing import TestCaseWithFactory
17
18from zope.security.proxy import removeSecurityProxy
19
20
21class TestBugHeatCalculator(TestCaseWithFactory):
22 """Tests for the BugHeatCalculator class."""
23 # If you change the way that bug heat is calculated, remember to update
24 # the description of how it is calculated at
25 # /lib/lp/bugs/help/bug-heat.html and
26 # https://help.launchpad.net/Bugs/BugHeat
27
28 layer = LaunchpadZopelessLayer
29
30 def setUp(self):
31 super(TestBugHeatCalculator, self).setUp()
32 self.bug = self.factory.makeBug()
33 self.calculator = BugHeatCalculator(self.bug)
34
35 def test__getHeatFromDuplicates(self):
36 # BugHeatCalculator._getHeatFromDuplicates() returns the bug
37 # heat generated by duplicates of a bug.
38 # By default, the bug has no heat from dupes
39 self.assertEqual(0, self.calculator._getHeatFromDuplicates())
40
41 # If adding duplicates, the heat generated by them will be n *
42 # BugHeatConstants.DUPLICATE, where n is the number of
43 # duplicates.
44 for i in range(5):
45 dupe = self.factory.makeBug()
46 dupe.duplicateof = self.bug
47
48 expected_heat = BugHeatConstants.DUPLICATE * 5
49 actual_heat = self.calculator._getHeatFromDuplicates()
50 self.assertEqual(
51 expected_heat, actual_heat,
52 "Heat from duplicates does not match expected heat. "
53 "Expected %s, got %s" % (expected_heat, actual_heat))
54
55 def test__getHeatFromAffectedUsers(self):
56 # BugHeatCalculator._getHeatFromAffectedUsers() returns the bug
57 # heat generated by users affected by the bug and by duplicate bugs.
58 # By default, the heat will be BugHeatConstants.AFFECTED_USER, since
59 # there will be one affected user (the user who filed the bug).
60 self.assertEqual(
61 BugHeatConstants.AFFECTED_USER,
62 self.calculator._getHeatFromAffectedUsers())
63
64 # As the number of affected users increases, the heat generated
65 # will be n * BugHeatConstants.AFFECTED_USER, where n is the number
66 # of affected users.
67 for i in range(5):
68 person = self.factory.makePerson()
69 self.bug.markUserAffected(person)
70
71 expected_heat = BugHeatConstants.AFFECTED_USER * 6
72 actual_heat = self.calculator._getHeatFromAffectedUsers()
73 self.assertEqual(
74 expected_heat, actual_heat,
75 "Heat from affected users does not match expected heat. "
76 "Expected %s, got %s" % (expected_heat, actual_heat))
77
78 # When our bug has duplicates, users affected by these duplicates
79 # are included in _getHeatFromAffectedUsers() of the main bug.
80 for i in range(3):
81 dupe = self.factory.makeBug()
82 dupe.duplicateof = self.bug
83 # Each bug reporter is by default also marked as being affected
84 # by the bug, so we have three additional affected users.
85 expected_heat += BugHeatConstants.AFFECTED_USER * 3
86
87 person = self.factory.makePerson()
88 dupe.markUserAffected(person)
89 expected_heat += BugHeatConstants.AFFECTED_USER
90 actual_heat = self.calculator._getHeatFromAffectedUsers()
91 self.assertEqual(
92 expected_heat, actual_heat,
93 "Heat from users affected by duplicate bugs does not match "
94 "expected heat. Expected %s, got %s"
95 % (expected_heat, actual_heat))
96
97 def test__getHeatFromSubscribers(self):
98 # BugHeatCalculator._getHeatFromSubscribers() returns the bug
99 # heat generated by users subscribed tothe bug.
100 # By default, the heat will be BugHeatConstants.SUBSCRIBER,
101 # since there will be one direct subscriber (the user who filed
102 # the bug).
103 self.assertEqual(
104 BugHeatConstants.SUBSCRIBER,
105 self.calculator._getHeatFromSubscribers())
106
107 # As the number of subscribers increases, the heat generated
108 # will be n * BugHeatConstants.SUBSCRIBER, where n is the number
109 # of subscribers.
110 for i in range(5):
111 person = self.factory.makePerson()
112 self.bug.subscribe(person, person)
113
114 expected_heat = BugHeatConstants.SUBSCRIBER * 6
115 actual_heat = self.calculator._getHeatFromSubscribers()
116 self.assertEqual(
117 expected_heat, actual_heat,
118 "Heat from subscribers does not match expected heat. "
119 "Expected %s, got %s" % (expected_heat, actual_heat))
120
121 # Subscribers from duplicates are included in the heat returned
122 # by _getHeatFromSubscribers()
123 dupe = self.factory.makeBug()
124 dupe.duplicateof = self.bug
125 expected_heat = BugHeatConstants.SUBSCRIBER * 7
126 actual_heat = self.calculator._getHeatFromSubscribers()
127 self.assertEqual(
128 expected_heat, actual_heat,
129 "Heat from subscribers (including duplicate-subscribers) "
130 "does not match expected heat. Expected %s, got %s" %
131 (expected_heat, actual_heat))
132
133 # Seting the bug to private will increase its heat from
134 # subscribers by 1 * BugHeatConstants.SUBSCRIBER, as the project
135 # owner will now be directly subscribed to it.
136 self.bug.setPrivate(True, self.bug.owner)
137 expected_heat = BugHeatConstants.SUBSCRIBER * 8
138 actual_heat = self.calculator._getHeatFromSubscribers()
139 self.assertEqual(
140 expected_heat, actual_heat,
141 "Heat from subscribers to private bug does not match expected "
142 "heat. Expected %s, got %s" % (expected_heat, actual_heat))
143
144 def test__getHeatFromPrivacy(self):
145 # BugHeatCalculator._getHeatFromPrivacy() returns the heat
146 # generated by the bug's private attribute. If the bug is
147 # public, this will be 0.
148 self.assertEqual(0, self.calculator._getHeatFromPrivacy())
149
150 # However, if the bug is private, _getHeatFromPrivacy() will
151 # return BugHeatConstants.PRIVACY.
152 self.bug.setPrivate(True, self.bug.owner)
153 self.assertEqual(
154 BugHeatConstants.PRIVACY, self.calculator._getHeatFromPrivacy())
155
156 def test__getHeatFromSecurity(self):
157 # BugHeatCalculator._getHeatFromSecurity() returns the heat
158 # generated by the bug's security_related attribute. If the bug
159 # is not security related, _getHeatFromSecurity() will return 0.
160 self.assertEqual(0, self.calculator._getHeatFromPrivacy())
161
162
163 # If, on the other hand, the bug is security_related,
164 # _getHeatFromSecurity() will return BugHeatConstants.SECURITY
165 self.bug.setSecurityRelated(True)
166 self.assertEqual(
167 BugHeatConstants.SECURITY, self.calculator._getHeatFromSecurity())
168
169 def test_getBugHeat(self):
170 # BugHeatCalculator.getBugHeat() returns the total heat for a
171 # given bug as the sum of the results of all _getHeatFrom*()
172 # methods.
173 # By default this will be (BugHeatConstants.AFFECTED_USER +
174 # BugHeatConstants.SUBSCRIBER) since there will be one
175 # subscriber and one affected user only.
176 expected_heat = (
177 BugHeatConstants.AFFECTED_USER + BugHeatConstants.SUBSCRIBER)
178 actual_heat = self.calculator.getBugHeat()
179 self.assertEqual(
180 expected_heat, actual_heat,
181 "Expected bug heat did not match actual bug heat. "
182 "Expected %s, got %s" % (expected_heat, actual_heat))
183
184 # Adding a duplicate and making the bug private and security
185 # related will increase its heat.
186 dupe = self.factory.makeBug()
187 dupe.duplicateof = self.bug
188 self.bug.setPrivate(True, self.bug.owner)
189 self.bug.setSecurityRelated(True)
190
191 expected_heat += (
192 BugHeatConstants.DUPLICATE +
193 BugHeatConstants.PRIVACY +
194 BugHeatConstants.SECURITY +
195 BugHeatConstants.AFFECTED_USER
196 )
197
198 # Adding the duplicate and making the bug private means it gets
199 # two new subscribers, the project owner and the duplicate's
200 # direct subscriber.
201 expected_heat += BugHeatConstants.SUBSCRIBER * 2
202 actual_heat = self.calculator.getBugHeat()
203 self.assertEqual(
204 expected_heat, actual_heat,
205 "Expected bug heat did not match actual bug heat. "
206 "Expected %s, got %s" % (expected_heat, actual_heat))
207
208 def test_getBugHeat_complete_bugs(self):
209 # Bug which are in a resolved status don't have heat at all.
210 complete_bug = self.factory.makeBug()
211 heat = BugHeatCalculator(complete_bug).getBugHeat()
212 self.assertNotEqual(
213 0, heat,
214 "Expected bug heat did not match actual bug heat. "
215 "Expected a positive value, got 0")
216 complete_bug.bugtasks[0].transitionToStatus(
217 BugTaskStatus.INVALID, complete_bug.owner)
218 heat = BugHeatCalculator(complete_bug).getBugHeat()
219 self.assertEqual(
220 0, heat,
221 "Expected bug heat did not match actual bug heat. "
222 "Expected %s, got %s" % (0, heat))
223
224 def test_getBugHeat_decay(self):
225 # Every day, a bug that wasn't touched has its heat reduced by 1%.
226 aging_bug = self.factory.makeBug()
227 fresh_heat = BugHeatCalculator(aging_bug).getBugHeat()
228 aging_bug.date_last_updated = (
229 aging_bug.date_last_updated - timedelta(days=1))
230 expected = int(fresh_heat * 0.99)
231 heat = BugHeatCalculator(aging_bug).getBugHeat()
232 self.assertEqual(
233 expected, heat,
234 "Expected bug heat did not match actual bug heat. "
235 "Expected %s, got %s" % (expected, heat))
236
237 def test_getBugHeat_activity(self):
238 # Bug heat increases by a quarter of the maximum bug heat divided by
239 # the number of days between the bug's creating and its last activity.
240 active_bug = removeSecurityProxy(self.factory.makeBug())
241 fresh_heat = BugHeatCalculator(active_bug).getBugHeat()
242 active_bug.date_last_updated = (
243 active_bug.date_last_updated - timedelta(days=10))
244 active_bug.datecreated = (active_bug.datecreated - timedelta(days=20))
245 active_bug.default_bugtask.target.setMaxBugHeat(100)
246 expected = int((fresh_heat * (0.99 ** 20)) + (100 * 0.25 / 20))
247 heat = BugHeatCalculator(active_bug).getBugHeat()
248 self.assertEqual(
249 expected, heat,
250 "Expected bug heat did not match actual bug heat. "
251 "Expected %s, got %s" % (expected, heat))
252
253
254
255def test_suite():
256 return unittest.TestLoader().loadTestsFromName(__name__)
2570
=== modified file 'lib/lp/bugs/tests/bugs-emailinterface.txt'
--- lib/lp/bugs/tests/bugs-emailinterface.txt 2010-05-27 13:51:06 +0000
+++ lib/lp/bugs/tests/bugs-emailinterface.txt 2010-06-14 20:18:33 +0000
@@ -1435,7 +1435,7 @@
1435 status foo1435 status foo
1436 ...1436 ...
1437 The 'status' command expects any of the following arguments:1437 The 'status' command expects any of the following arguments:
1438 new, incomplete, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased1438 new, incomplete, opinion, invalid, wontfix, expired, confirmed, triaged, inprogress, fixcommitted, fixreleased
1439 <BLANKLINE>1439 <BLANKLINE>
1440 For example:1440 For example:
1441 <BLANKLINE>1441 <BLANKLINE>
14421442
=== modified file 'lib/lp/bugs/tests/bugtarget-bugcount.txt'
--- lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-04-15 13:26:33 +0000
+++ lib/lp/bugs/tests/bugtarget-bugcount.txt 2010-06-14 20:18:33 +0000
@@ -11,6 +11,7 @@
11 ... print status.name11 ... print status.name
12 NEW12 NEW
13 INCOMPLETE13 INCOMPLETE
14 OPINION
14 INVALID15 INVALID
15 WONTFIX16 WONTFIX
16 EXPIRED17 EXPIRED
@@ -54,6 +55,7 @@
54 ... print_count_difference(new_bug_counts, old_counts, status)55 ... print_count_difference(new_bug_counts, old_counts, status)
55 NEW: 5 bug(s) more56 NEW: 5 bug(s) more
56 INCOMPLETE: 5 bug(s) more57 INCOMPLETE: 5 bug(s) more
58 OPINION: 5 bug(s) more
57 INVALID: 5 bug(s) more59 INVALID: 5 bug(s) more
58 WONTFIX: 5 bug(s) more60 WONTFIX: 5 bug(s) more
59 EXPIRED: 5 bug(s) more61 EXPIRED: 5 bug(s) more
6062
=== modified file 'lib/lp/bugs/tests/test_bugheat.py'
--- lib/lp/bugs/tests/test_bugheat.py 2010-05-27 13:56:03 +0000
+++ lib/lp/bugs/tests/test_bugheat.py 2010-06-14 20:18:33 +0000
@@ -5,114 +5,13 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8import pytz
9import transaction
10import unittest8import unittest
11from datetime import datetime9
12
13from zope.component import getUtility
14
15from canonical.launchpad.scripts.tests import run_script
16from canonical.testing import LaunchpadZopelessLayer10from canonical.testing import LaunchpadZopelessLayer
1711
18from lp.bugs.adapters.bugchange import BugDescriptionChange
19from lp.bugs.interfaces.bugjob import ICalculateBugHeatJobSource
20from lp.bugs.model.bugheat import CalculateBugHeatJob
21from lp.bugs.scripts.bugheat import BugHeatCalculator
22from lp.testing import TestCaseWithFactory
23from lp.testing.factory import LaunchpadObjectFactory12from lp.testing.factory import LaunchpadObjectFactory
2413
2514
26class CalculateBugHeatJobTestCase(TestCaseWithFactory):
27 """Test case for CalculateBugHeatJob."""
28
29 layer = LaunchpadZopelessLayer
30
31 def setUp(self):
32 super(CalculateBugHeatJobTestCase, self).setUp()
33 self.bug = self.factory.makeBug()
34
35 # NB: This looks like it should go in the teardown, however
36 # creating the bug causes a job to be added for it. We clear
37 # this out so that our tests are consistent.
38 self._completeJobsAndAssertQueueEmpty()
39
40 def _completeJobsAndAssertQueueEmpty(self):
41 """Make sure that all the CalculateBugHeatJobs are completed."""
42 for bug_job in getUtility(ICalculateBugHeatJobSource).iterReady():
43 bug_job.job.start()
44 bug_job.job.complete()
45 self.assertEqual(0, self._getJobCount())
46
47 def _getJobCount(self):
48 """Return the number of CalculateBugHeatJobs in the queue."""
49 return len(self._getJobs())
50
51 def _getJobs(self):
52 """Return the pending CalculateBugHeatJobs as a list."""
53 return list(CalculateBugHeatJob.iterReady())
54
55 def test_run(self):
56 # CalculateBugHeatJob.run() sets calculates and sets the heat
57 # for a bug.
58 job = CalculateBugHeatJob.create(self.bug)
59 bug_heat_calculator = BugHeatCalculator(self.bug)
60
61 job.run()
62 self.assertEqual(
63 bug_heat_calculator.getBugHeat(), self.bug.heat)
64
65 def test_utility(self):
66 # CalculateBugHeatJobSource is a utility for acquiring
67 # CalculateBugHeatJobs.
68 utility = getUtility(ICalculateBugHeatJobSource)
69 self.assertTrue(
70 ICalculateBugHeatJobSource.providedBy(utility))
71
72 def test_create_only_creates_one(self):
73 # If there's already a CalculateBugHeatJob for a bug,
74 # CalculateBugHeatJob.create() won't create a new one.
75 job = CalculateBugHeatJob.create(self.bug)
76
77 # There will now be one job in the queue.
78 self.assertEqual(1, self._getJobCount())
79
80 new_job = CalculateBugHeatJob.create(self.bug)
81
82 # The two jobs will in fact be the same job.
83 self.assertEqual(job, new_job)
84
85 # And the queue will still have a length of 1.
86 self.assertEqual(1, self._getJobCount())
87
88 def test_cronscript_succeeds(self):
89 # The calculate-bug-heat cronscript will run all pending
90 # CalculateBugHeatJobs.
91 CalculateBugHeatJob.create(self.bug)
92 transaction.commit()
93
94 retcode, stdout, stderr = run_script(
95 'cronscripts/calculate-bug-heat.py', [],
96 expect_returncode=0)
97 self.assertEqual('', stdout)
98 self.assertIn(
99 'INFO Ran 1 CalculateBugHeatJob jobs.\n', stderr)
100
101 def test_getOopsVars(self):
102 # BugJobDerived.getOopsVars() returns the variables to be used
103 # when logging an OOPS for a bug job. We test this using
104 # CalculateBugHeatJob because BugJobDerived doesn't let us
105 # create() jobs.
106 job = CalculateBugHeatJob.create(self.bug)
107 vars = job.getOopsVars()
108
109 # The Bug ID, BugJob ID and BugJob type will be returned by
110 # getOopsVars().
111 self.assertIn(('bug_id', self.bug.id), vars)
112 self.assertIn(('bug_job_id', job.context.id), vars)
113 self.assertIn(('bug_job_type', job.context.job_type.title), vars)
114
115
116class MaxHeatByTargetBase:15class MaxHeatByTargetBase:
117 """Base class for testing a bug target's max_bug_heat attribute."""16 """Base class for testing a bug target's max_bug_heat attribute."""
11817
11918
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py 2010-06-12 13:34:11 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
@@ -309,7 +309,10 @@
309 'name',309 'name',
310 'description',310 'description',
311 'owner',311 'owner',
312 'build_daily'
312 ])313 ])
314 daily_build_archive = Choice(vocabulary='TargetPPAs',
315 title=u'Daily build archive')
313 distros = List(316 distros = List(
314 Choice(vocabulary='BuildableDistroSeries'),317 Choice(vocabulary='BuildableDistroSeries'),
315 title=u'Default Distribution series')318 title=u'Default Distribution series')
@@ -318,10 +321,16 @@
318 description=u'The text of the recipe.')321 description=u'The text of the recipe.')
319322
320323
324
321class RecipeTextValidatorMixin:325class RecipeTextValidatorMixin:
322 """Class to validate that the Source Package Recipe text is valid."""326 """Class to validate that the Source Package Recipe text is valid."""
323327
324 def validate(self, data):328 def validate(self, data):
329 if data['build_daily']:
330 if len(data['distros']) == 0:
331 self.setFieldError(
332 'distros',
333 'You must specify at least one series for daily builds.')
325 try:334 try:
326 parser = RecipeParser(data['recipe_text'])335 parser = RecipeParser(data['recipe_text'])
327 parser.parse()336 parser.parse()
@@ -343,7 +352,8 @@
343 def initial_values(self):352 def initial_values(self):
344 return {353 return {
345 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,354 'recipe_text': MINIMAL_RECIPE_TEXT % self.context.bzr_identity,
346 'owner': self.user}355 'owner': self.user,
356 'build_daily': False}
347357
348 @property358 @property
349 def cancel_url(self):359 def cancel_url(self):
@@ -357,7 +367,8 @@
357 source_package_recipe = getUtility(367 source_package_recipe = getUtility(
358 ISourcePackageRecipeSource).new(368 ISourcePackageRecipeSource).new(
359 self.user, self.user, data['name'], recipe,369 self.user, self.user, data['name'], recipe,
360 data['description'], data['distros'])370 data['description'], data['distros'],
371 data['daily_build_archive'], data['build_daily'])
361 except ForbiddenInstruction:372 except ForbiddenInstruction:
362 # XXX: bug=592513 We shouldn't be hardcoding "run" here.373 # XXX: bug=592513 We shouldn't be hardcoding "run" here.
363 self.setFieldError(374 self.setFieldError(
364375
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-12 13:52:31 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
@@ -69,6 +69,14 @@
6969
70 layer = DatabaseFunctionalLayer70 layer = DatabaseFunctionalLayer
7171
72 def makeBranch(self):
73 product = self.factory.makeProduct(
74 name='ratatouille', displayname='Ratatouille')
75 branch = self.factory.makeBranch(
76 owner=self.chef, product=product, name='veggies')
77 self.factory.makeSourcePackage(sourcepackagename='ratatouille')
78 return branch
79
72 def test_create_new_recipe_not_logged_in(self):80 def test_create_new_recipe_not_logged_in(self):
73 from canonical.launchpad.testing.pages import setupBrowser81 from canonical.launchpad.testing.pages import setupBrowser
74 product = self.factory.makeProduct(82 product = self.factory.makeProduct(
@@ -85,11 +93,7 @@
85 Unauthorized, browser.getLink('Create packaging recipe').click)93 Unauthorized, browser.getLink('Create packaging recipe').click)
8694
87 def test_create_new_recipe(self):95 def test_create_new_recipe(self):
88 product = self.factory.makeProduct(96 branch = self.makeBranch()
89 name='ratatouille', displayname='Ratatouille')
90 branch = self.factory.makeBranch(
91 owner=self.chef, product=product, name='veggies')
92
93 # A new recipe can be created from the branch page.97 # A new recipe can be created from the branch page.
94 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)98 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
95 browser.getLink('Create packaging recipe').click()99 browser.getLink('Create packaging recipe').click()
@@ -97,6 +101,7 @@
97 browser.getControl(name='field.name').value = 'daily'101 browser.getControl(name='field.name').value = 'daily'
98 browser.getControl('Description').value = 'Make some food!'102 browser.getControl('Description').value = 'Make some food!'
99 browser.getControl('Secret Squirrel').click()103 browser.getControl('Secret Squirrel').click()
104 browser.getControl('Build daily').click()
100 browser.getControl('Create Recipe').click()105 browser.getControl('Create Recipe').click()
101106
102 pattern = """\107 pattern = """\
@@ -107,9 +112,11 @@
107 Make some food!112 Make some food!
108113
109 Recipe information114 Recipe information
115 Build daily: True
110 Owner: Master Chef116 Owner: Master Chef
111 Base branch: lp://dev/~chef/ratatouille/veggies117 Base branch: lp://dev/~chef/ratatouille/veggies
112 Debian version: 1.0118 Debian version: 1.0
119 Daily build archive: Secret PPA
113 Distribution series: Secret Squirrel120 Distribution series: Secret Squirrel
114 .*121 .*
115122
@@ -168,10 +175,7 @@
168 def test_create_recipe_bad_text(self):175 def test_create_recipe_bad_text(self):
169 # If a user tries to create source package recipe with bad text, they176 # If a user tries to create source package recipe with bad text, they
170 # should get an error.177 # should get an error.
171 product = self.factory.makeProduct(178 branch = self.makeBranch()
172 name='ratatouille', displayname='Ratatouille')
173 branch = self.factory.makeBranch(
174 owner=self.chef, product=product, name='veggies')
175179
176 # A new recipe can be created from the branch page.180 # A new recipe can be created from the branch page.
177 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)181 browser = self.getUserBrowser(canonical_url(branch), user=self.chef)
@@ -186,6 +190,17 @@
186 extract_text(find_tags_by_class(browser.contents, 'message')[1]),190 extract_text(find_tags_by_class(browser.contents, 'message')[1]),
187 'The recipe text is not a valid bzr-builder recipe.')191 'The recipe text is not a valid bzr-builder recipe.')
188192
193 def test_create_recipe_no_distroseries(self):
194 browser = self.getViewBrowser(self.makeBranch(), '+new-recipe')
195 browser.getControl(name='field.name').value = 'daily'
196 browser.getControl('Description').value = 'Make some food!'
197
198 browser.getControl('Build daily').click()
199 browser.getControl('Create Recipe').click()
200 self.assertEqual(
201 extract_text(find_tags_by_class(browser.contents, 'message')[1]),
202 'You must specify at least one series for daily builds.')
203
189 def test_create_dupe_recipe(self):204 def test_create_dupe_recipe(self):
190 # You shouldn't be able to create a duplicate recipe owned by the same205 # You shouldn't be able to create a duplicate recipe owned by the same
191 # person with the same name.206 # person with the same name.
@@ -228,7 +243,11 @@
228 recipe = self.factory.makeSourcePackageRecipe(243 recipe = self.factory.makeSourcePackageRecipe(
229 owner=self.chef, registrant=self.chef,244 owner=self.chef, registrant=self.chef,
230 name=u'things', description=u'This is a recipe',245 name=u'things', description=u'This is a recipe',
231 distroseries=self.squirrel, branches=[veggie_branch])246 distroseries=self.squirrel, branches=[veggie_branch],
247 daily_build_archive=self.ppa)
248 self.factory.makeArchive(
249 distribution=self.ppa.distribution, name='ppa2',
250 displayname="PPA 2", owner=self.chef)
232251
233 meat_path = meat_branch.bzr_identity252 meat_path = meat_branch.bzr_identity
234253
@@ -240,6 +259,7 @@
240 MINIMAL_RECIPE_TEXT % meat_path)259 MINIMAL_RECIPE_TEXT % meat_path)
241 browser.getControl('Secret Squirrel').click()260 browser.getControl('Secret Squirrel').click()
242 browser.getControl('Mumbly Midget').click()261 browser.getControl('Mumbly Midget').click()
262 browser.getControl('PPA 2').click()
243 browser.getControl('Update Recipe').click()263 browser.getControl('Update Recipe').click()
244264
245 pattern = """\265 pattern = """\
@@ -250,9 +270,12 @@
250 This is stuff270 This is stuff
251271
252 Recipe information272 Recipe information
273 Build daily: False
253 Owner: Master Chef274 Owner: Master Chef
254 Base branch: lp://dev/~chef/ratatouille/meat275 Base branch: lp://dev/~chef/ratatouille/meat
255 Debian version: 1.0276 Debian version: 1.0
277 Daily build archive:
278 PPA 2
256 Distribution series: Mumbly Midget279 Distribution series: Mumbly Midget
257 .*280 .*
258281
@@ -357,9 +380,13 @@
357 This is stuff380 This is stuff
358381
359 Recipe information382 Recipe information
383 Build daily:
384 False
360 Owner: Master Chef385 Owner: Master Chef
361 Base branch: lp://dev/~chef/ratatouille/meat386 Base branch: lp://dev/~chef/ratatouille/meat
362 Debian version: 1.0387 Debian version: 1.0
388 Daily build archive:
389 Secret PPA
363 Distribution series: Mumbly Midget390 Distribution series: Mumbly Midget
364 .*391 .*
365392
@@ -389,9 +416,11 @@
389 This recipe .*changes.416 This recipe .*changes.
390417
391 Recipe information418 Recipe information
419 Build daily: False
392 Owner: Master Chef420 Owner: Master Chef
393 Base branch: lp://dev/~chef/chocolate/cake421 Base branch: lp://dev/~chef/chocolate/cake
394 Debian version: 1.0422 Debian version: 1.0
423 Daily build archive: Secret PPA
395 Distribution series: Secret Squirrel424 Distribution series: Secret Squirrel
396425
397 Build records426 Build records
398427
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2010-06-10 07:55:54 +0000
+++ lib/lp/code/configure.zcml 2010-06-14 20:18:33 +0000
@@ -1052,6 +1052,7 @@
1052 set_attributes="1052 set_attributes="
1053 build_daily1053 build_daily
1054 builder_recipe1054 builder_recipe
1055 daily_build_archive
1055 date_last_modified1056 date_last_modified
1056 description1057 description
1057 distroseries1058 distroseries
10581059
=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-11 05:05:52 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py 2010-06-14 20:18:33 +0000
@@ -95,7 +95,7 @@
95 " build a source package for"),95 " build a source package for"),
96 readonly=False)96 readonly=False)
97 build_daily = Bool(97 build_daily = Bool(
98 title=_("If true, the recipe should be built daily."))98 title=_("Build daily"))
9999
100 name = exported(TextLine(100 name = exported(TextLine(
101 title=_("Name"), required=True,101 title=_("Name"), required=True,
102102
=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-04-28 21:18:13 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt 2010-06-14 20:18:33 +0000
@@ -36,6 +36,11 @@
36 <div class="portlet">36 <div class="portlet">
37 <h2>Recipe information</h2>37 <h2>Recipe information</h2>
38 <div class="two-column-list">38 <div class="two-column-list">
39 <dl id="build_daily">
40 <dt>Build daily:</dt>
41 <dd tal:content="context/build_daily" />
42 </dl>
43
39 <dl id="owner">44 <dl id="owner">
40 <dt>Owner:</dt>45 <dt>Owner:</dt>
41 <dd tal:content="structure context/owner/fmt:link" />46 <dd tal:content="structure context/owner/fmt:link" />
@@ -48,6 +53,14 @@
48 <dt>Debian version:</dt>53 <dt>Debian version:</dt>
49 <dd tal:content="context/deb_version_template" />54 <dd tal:content="context/deb_version_template" />
50 </dl>55 </dl>
56 <dl id="daily_build_archive">
57 <dt>Daily build archive:</dt>
58 <dd tal:content="structure context/daily_build_archive/fmt:link"
59 tal:condition="context/daily_build_archive">
60 </dd>
61 <dd tal:condition="not: context/daily_build_archive">None</dd>
62 </dl>
63
51 <dl id="distros">64 <dl id="distros">
52 <dt>Distribution series:</dt>65 <dt>Distribution series:</dt>
53 <dd>66 <dd>
5467
=== modified file 'utilities/report-database-stats.py'
--- utilities/report-database-stats.py 2010-04-29 12:38:05 +0000
+++ utilities/report-database-stats.py 2010-06-14 20:18:33 +0000
@@ -72,12 +72,22 @@
7272
7373
74def get_cpu_stats(cur, options):74def get_cpu_stats(cur, options):
75 # This query calculates the averate cpu utilization from the
76 # samples. It assumes samples are taken at regular intervals over
77 # the period.
75 query = """78 query = """
76 SELECT avg(cpu), username FROM DatabaseCpuStats79 SELECT (
80 CAST(SUM(cpu) AS float) / (
81 SELECT COUNT(DISTINCT date_created) FROM DatabaseCpuStats
82 WHERE
83 date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
84 - CAST (%s AS interval))
85 ) AS avg_cpu, username
86 FROM DatabaseCpuStats
77 WHERE date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'87 WHERE date_created >= (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
78 - CAST(%s AS interval))88 - CAST(%s AS interval))
79 GROUP BY username89 GROUP BY username
80 """ % sqlvalues(options.since_interval)90 """ % sqlvalues(options.since_interval, options.since_interval)
8191
82 cur.execute(query)92 cur.execute(query)
8393
@@ -107,16 +117,20 @@
107 tables = get_table_stats(cur, options)117 tables = get_table_stats(cur, options)
108 arbitrary_table = list(tables)[0]118 arbitrary_table = list(tables)[0]
109 interval = arbitrary_table.date_end - arbitrary_table.date_start119 interval = arbitrary_table.date_end - arbitrary_table.date_start
110 per_minute = interval.days * 24 * 60 + interval.seconds / 60.0120 per_second = float(interval.days * 24 * 60 * 60 + interval.seconds)
111121
112 print "== Most Read Tables =="122 print "== Most Read Tables =="
113 print123 print
124 # These match the pg_user_table_stats view. schemaname is the
125 # namespace (normally 'public'), relname is the table (relation)
126 # name. total_tup_red is the total number of rows read.
127 # idx_tup_fetch is the number of rows looked up using an index.
114 tables_sort = ['total_tup_read', 'idx_tup_fetch', 'schemaname', 'relname']128 tables_sort = ['total_tup_read', 'idx_tup_fetch', 'schemaname', 'relname']
115 most_read_tables = sorted(129 most_read_tables = sorted(
116 tables, key=attrgetter(*tables_sort), reverse=True)130 tables, key=attrgetter(*tables_sort), reverse=True)
117 for table in most_read_tables[:options.limit]:131 for table in most_read_tables[:options.limit]:
118 print "%40s || %10.2f tuples/min" % (132 print "%40s || %10.2f tuples/sec" % (
119 table.relname, table.total_tup_read / per_minute)133 table.relname, table.total_tup_read / per_second)
120 print134 print
121135
122 print "== Most Written Tables =="136 print "== Most Written Tables =="
@@ -126,15 +140,15 @@
126 most_written_tables = sorted(140 most_written_tables = sorted(
127 tables, key=attrgetter(*tables_sort), reverse=True)141 tables, key=attrgetter(*tables_sort), reverse=True)
128 for table in most_written_tables[:options.limit]:142 for table in most_written_tables[:options.limit]:
129 print "%40s || %10.2f tuples/min" % (143 print "%40s || %10.2f tuples/sec" % (
130 table.relname, table.total_tup_written / per_minute)144 table.relname, table.total_tup_written / per_second)
131 print145 print
132146
133 user_cpu = get_cpu_stats(cur, options)147 user_cpu = get_cpu_stats(cur, options)
134 print "== Most Active Users =="148 print "== Most Active Users =="
135 print149 print
136 for cpu, username in sorted(user_cpu, reverse=True)[:options.limit]:150 for cpu, username in sorted(user_cpu, reverse=True)[:options.limit]:
137 print "%40s || %6.2f%% CPU" % (username, float(cpu) / 100)151 print "%40s || %10.2f%% CPU" % (username, float(cpu) / 10)
138152
139153
140if __name__ == '__main__':154if __name__ == '__main__':