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