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 | 2010-05-28 | Approve on 2010-06-03 |
|
Review via email:
|
|||
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...