Merge lp:~rockstar/launchpad/branch-index-redesign into lp:launchpad

Proposed by Paul Hummer
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~rockstar/launchpad/branch-index-redesign
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~rockstar/launchpad/branch-index-redesign
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) ui Approve
Barry Warsaw (community) ui* Approve
Aaron Bentley (community) Approve
Review via email: mp+12061@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

Hi Aaron-

  First of all, I apologize for the size of this branch. I couldn't see a sane
way of splitting it up.

  This branch is a long time coming. It's the re-design of the branch index
page. I'm 99% sure I got all the failing tests and fixed them (it's gone
through ec2 at least 5 times). While working on that though, I found that our
tests are far too dependent on the html layout of pages, instead of the actual
content. I plan to fix this soon by moving a lot of what we're testing out
into unittests (and kill the page tests).

  There are some flakes errors being raised about not being able to import lazr
packages, but I think flakes is just stupid.

 reviewer abentley

Cheers,
Paul

Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

 status approved

Paul Hummer wrote:
> This branch is ...the re-design of the branch index
> page.

(The review diff is wildly inaccurate.)

As discussed on IRC, please make the following changes:

Add enabled_with_permission to edit_import.

Please remove the lolspeak comment from lib/lp/code/browser/configure.zcml

Please stop hiding merges into import branches.

Please remove the outer div of the nested tal:condition from the top of
branch-management.pt

Aside from that, this is good to land.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkqzszwACgkQ0F+nu1YWqI2rqwCeLvmgAsZCRqnTQRc4ApBIXgC+
XZUAn1yJBLbVkHvmy1/9HVy2rTvHkJZh
=jkPY
-----END PGP SIGNATURE-----

review: Approve
Revision history for this message
Paul Hummer (rockstar) wrote :
Revision history for this message
Barry Warsaw (barry) wrote :

As we discussed on IRC, the wrapping of the H1 heading is ugly and should be fixed, but is outside the scope of this branch. Likely the extras slot is in the wrong place. You agreed to file a bug on that issue before you land this branch so that we can address that post-3.0.

Other than that, it looks great!

review: Approve (ui*)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Paul,

This looks nice. I only have two small changes that I would like.

 1. Move the public/private info to the top of the sidebar, so its location is more similar to bugs and teams.
 2. For private branches, add a second line to the public/private box to explain what the privacy means. For example, private teams now say "Viewable by team members", and I assume the branch subscriber and owner can view a private branch.

-Edwin

review: Approve (ui)
Revision history for this message
Jonathan Lange (jml) wrote :

On Sat, Sep 19, 2009 at 12:50 AM, Paul Hummer <email address hidden> wrote:
> Paul Hummer has proposed merging lp:~rockstar/launchpad/branch-index-redesign into lp:launchpad/devel.
>
> Requested reviews:
>    Aaron Bentley (abentley)
>
> Hi Aaron-
>
>  First of all, I apologize for the size of this branch.  I couldn't see a sane
> way of splitting it up.
>
>  This branch is a long time coming.  It's the re-design of the branch index
> page.  I'm 99% sure I got all the failing tests and fixed them (it's gone
> through ec2 at least 5 times).  While working on that though, I found that our
> tests are far too dependent on the html layout of pages, instead of the actual
> content.  I plan to fix this soon by moving a lot of what we're testing out
> into unittests (and kill the page tests).
>
>  There are some flakes errors being raised about not being able to import lazr
> packages, but I think flakes is just stupid.
>
>  reviewer abentley

I've got a few questions that I can't figure out from the screenshots.

  - If it's a mirrored branch, and the mirror has failed, then what
does the page look like?

  - Where does the "Branch content" link go to? I hope it's to file
browsing on loggerhead

  - I think it's a negative that there's absolutely no information
about the last revision on this page.

  - There should also be a link to the series of the branch, if the
branch is linked to a series.

  - How do I delete a branch?

  - How do I change the status?

My flight's leaving now, but I'd really like to talk about this
further. Sorry for being so terse.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2009-08-05 23:08:37 +0000
3+++ .bzrignore 2009-09-10 06:11:21 +0000
4@@ -50,3 +50,4 @@
5 ./_pythonpath.py
6 ./production-configs
7 bzr.dev
8+_trial_temp
9
10=== modified file 'Makefile'
11--- Makefile 2009-08-21 17:50:58 +0000
12+++ Makefile 2009-09-17 12:56:07 +0000
13@@ -90,6 +90,15 @@
14 @echo
15 @echo "Running the JavaScript integration test suite"
16 @echo
17+ bin/test $(VERBOSITY) --layer=BugsWindmillLayer
18+ bin/test $(VERBOSITY) --layer=CodeWindmillLayer
19+
20+jscheck_functest: build
21+ # Run the old functest Windmill integration tests. The test runner
22+ # takes care of setting up the test environment.
23+ @echo
24+ @echo "Running Windmill funtest integration test suite"
25+ @echo
26 bin/jstest
27
28 check_mailman: build
29
30=== modified file 'buildmailman.py'
31--- buildmailman.py 2009-07-17 00:26:05 +0000
32+++ buildmailman.py 2009-09-11 02:17:29 +0000
33@@ -41,6 +41,14 @@
34 else:
35 return 0
36
37+ # sys.path_importer_cache is a mapping of elements of sys.path to importer
38+ # objects used to handle them. In Python2.5+ when an element of sys.path is
39+ # found to not exist on disk, a NullImporter is created and cached - this
40+ # causes Python to never bother re-inspecting the disk for that path
41+ # element. We must clear that cache element so that our second attempt to
42+ # import MailMan after building it will actually check the disk.
43+ del sys.path_importer_cache[mailman_path]
44+
45 # Make sure the target directories exist and have the correct
46 # permissions, otherwise configure will complain.
47 user, group = as_username_groupname(config.mailman.build_user_group)
48
49=== modified file 'buildout-templates/bin/test.in'
50--- buildout-templates/bin/test.in 2009-08-10 22:08:05 +0000
51+++ buildout-templates/bin/test.in 2009-09-17 17:42:25 +0000
52@@ -134,14 +134,13 @@
53 from zope.testing import testrunner
54 from zope.testing.testrunner import options
55
56-defaults = [
57+defaults = {
58 # Find tests in the tests and ftests directories
59- '--tests-pattern=^f?tests$',
60- '--test-path=${buildout:directory}/lib',
61- '--package=canonical',
62- '--package=lp',
63- '--layer=!MailmanLayer',
64- ]
65+ 'tests_pattern': '^f?tests$',
66+ 'test_path': ['${buildout:directory}/lib'],
67+ 'package': ['canonical', 'lp', 'devscripts'],
68+ 'layer': ['!(MailmanLayer|WindmillLayer)'],
69+ }
70
71 # Monkey-patch os.listdir to randomise the results
72 original_listdir = os.listdir
73@@ -202,7 +201,22 @@
74 options.parser.add_option(
75 '--subunit', action='callback', callback=use_subunit)
76
77- local_options = options.get_options(args=args, defaults=defaults)
78+ # tests_pattern is a regexp, so the parsed value is hard to compare
79+ # with the default value in the loop below.
80+ options.parser.defaults['tests_pattern'] = defaults['tests_pattern']
81+ local_options = options.get_options(args=args)
82+ # Set our default options, if the options aren't specified.
83+ for name, value in defaults.items():
84+ parsed_option = getattr(local_options, name)
85+ if ((parsed_option == []) or
86+ (parsed_option == options.parser.defaults.get(name))):
87+ # The option probably wasn't specified on the command line,
88+ # let's replace it with our default value. It could be that
89+ # the real default (as specified in
90+ # zope.testing.testrunner.options) was specified, and we
91+ # shouldn't replace it with our default, but it's such and
92+ # edge case, so we don't have to care about it.
93+ options.parser.defaults[name] = value
94
95 # Turn on Layer profiling if requested.
96 from canonical.testing import profiled
97@@ -216,7 +230,7 @@
98 try:
99 there = os.getcwd()
100 os.chdir('${buildout:directory}')
101- result = testrunner.run(defaults)
102+ result = testrunner.run([])
103 finally:
104 os.chdir(there)
105 # Cribbed from sourcecode/zope/test.py - avoid spurious error during exit.
106
107=== modified file 'configs/development/launchpad-lazr.conf'
108--- configs/development/launchpad-lazr.conf 2009-08-19 12:28:32 +0000
109+++ configs/development/launchpad-lazr.conf 2009-09-14 19:32:10 +0000
110@@ -145,6 +145,7 @@
111 restricted_upload_port: 58095
112 restricted_download_port: 58085
113 restricted_download_url: http://launchpad.dev:58085/
114+use_https = False
115
116 [librarian_server]
117 root: /var/tmp/fatsam
118
119=== modified file 'configs/development/launchpad.conf'
120--- configs/development/launchpad.conf 2009-06-12 16:36:02 +0000
121+++ configs/development/launchpad.conf 2009-09-11 18:14:50 +0000
122@@ -66,7 +66,7 @@
123 </eventlog>
124
125 <logger>
126- name zc.zservertracelog
127+ name zc.tracelog
128 propagate false
129
130 <logfile>
131
132=== modified file 'configs/test-playground/launchpad.conf'
133--- configs/test-playground/launchpad.conf 2008-11-10 16:12:10 +0000
134+++ configs/test-playground/launchpad.conf 2009-09-11 18:14:50 +0000
135@@ -66,7 +66,7 @@
136 </eventlog>
137
138 <logger>
139- name zc.zservertracelog
140+ name zc.tracelog
141 propagate false
142
143 <logfile>
144
145=== modified file 'configs/testrunner-appserver/launchpad.conf'
146--- configs/testrunner-appserver/launchpad.conf 2009-05-12 21:22:02 +0000
147+++ configs/testrunner-appserver/launchpad.conf 2009-09-11 18:14:50 +0000
148@@ -39,7 +39,7 @@
149 </eventlog>
150
151 <logger>
152- name zc.zservertracelog
153+ name zc.tracelog
154 propagate false
155
156 <logfile>
157
158=== modified file 'configs/testrunner/launchpad-lazr.conf'
159--- configs/testrunner/launchpad-lazr.conf 2009-08-15 03:43:17 +0000
160+++ configs/testrunner/launchpad-lazr.conf 2009-08-28 21:02:50 +0000
161@@ -159,6 +159,9 @@
162 oops_prefix: TMPCJ
163 error_dir: /var/tmp/codehosting.test
164
165+[update_preview_diffs]
166+oops_prefix: TUPD
167+error_dir: /var/tmp/codehosting.test
168
169 [personalpackagearchive]
170 root: /var/tmp/ppa.test/
171
172=== modified file 'cronscripts/create_merge_proposals.py'
173--- cronscripts/create_merge_proposals.py 2009-06-24 20:52:01 +0000
174+++ cronscripts/create_merge_proposals.py 2009-09-03 19:46:42 +0000
175@@ -26,7 +26,7 @@
176 def main(self):
177 globalErrorUtility.configure('create_merge_proposals')
178 job_source = getUtility(ICreateMergeProposalJobSource)
179- runner = JobRunner.fromReady(job_source)
180+ runner = JobRunner.fromReady(job_source, self.logger)
181 runner.runAll()
182 self.logger.info(
183 'Ran %d CreateMergeProposalJobs.' % len(runner.completed_jobs))
184
185=== modified file 'cronscripts/mpcreationjobs.py'
186--- cronscripts/mpcreationjobs.py 2009-06-24 20:52:01 +0000
187+++ cronscripts/mpcreationjobs.py 2009-09-03 19:04:28 +0000
188@@ -31,7 +31,7 @@
189 def main(self):
190 globalErrorUtility.configure('mpcreationjobs')
191 job_source = getUtility(IMergeProposalCreatedJobSource)
192- runner = JobRunner.fromReady(job_source)
193+ runner = JobRunner.fromReady(job_source, self.logger)
194 server = get_scanner_server()
195 server.setUp()
196 try:
197
198=== modified file 'cronscripts/parse-librarian-apache-access-logs.py'
199--- cronscripts/parse-librarian-apache-access-logs.py 2009-08-31 15:01:54 +0000
200+++ cronscripts/parse-librarian-apache-access-logs.py 2009-09-11 12:11:04 +0000
201@@ -16,8 +16,6 @@
202
203 __metaclass__ = type
204
205-import os
206-
207 # pylint: disable-msg=W0403
208 import _pythonpath
209
210@@ -26,55 +24,36 @@
211 from storm.sqlobject import SQLObjectNotFound
212
213 from canonical.config import config
214-from lp.services.worlddata.interfaces.country import ICountrySet
215 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
216-from lp.services.scripts.base import LaunchpadCronScript
217-from lp.services.apachelogparser.base import (
218- create_or_update_parsedlog_entry, get_files_to_parse, parse_file)
219 from canonical.launchpad.scripts.librarian_apache_log_parser import (
220 DBUSER, get_library_file_id)
221-from canonical.launchpad.webapp.interfaces import NotFoundError
222-
223-
224-class ParseLibrarianApacheLogs(LaunchpadCronScript):
225-
226- def main(self):
227- root = config.librarianlogparser.logs_root
228- files_to_parse = get_files_to_parse(root, os.listdir(root))
229-
230- libraryfilealias_set = getUtility(ILibraryFileAliasSet)
231- country_set = getUtility(ICountrySet)
232- for fd, position in files_to_parse.items():
233- downloads, parsed_bytes = parse_file(
234- fd, position, self.logger, get_library_file_id)
235- # Use a while loop here because we want to pop items from the dict
236- # in order to free some memory as we go along. This is a good
237- # thing here because the downloads dict may get really huge.
238- while downloads:
239- file_id, daily_downloads = downloads.popitem()
240- try:
241- lfa = libraryfilealias_set[file_id]
242- except SQLObjectNotFound:
243- # This file has been deleted from the librarian, so don't
244- # try to store download counters for it.
245- continue
246- for day, country_downloads in daily_downloads.items():
247- for country_code, count in country_downloads.items():
248- try:
249- country = country_set[country_code]
250- except NotFoundError:
251- # We don't know the country for the IP address
252- # where this request originated.
253- country = None
254- lfa.updateDownloadCount(day, country, count)
255- fd.seek(0)
256- first_line = fd.readline()
257- fd.close()
258- create_or_update_parsedlog_entry(first_line, parsed_bytes)
259- self.txn.commit()
260- self.logger.info('Finished parsing %s' % fd)
261-
262- self.logger.info('Done parsing apache log files for librarian')
263+from lp.services.apachelogparser.script import ParseApacheLogs
264+
265+
266+class ParseLibrarianApacheLogs(ParseApacheLogs):
267+ """An Apache log parser for LibraryFileAlias downloads."""
268+
269+ def setUpUtilities(self):
270+ """See `ParseApacheLogs`."""
271+ self.libraryfilealias_set = getUtility(ILibraryFileAliasSet)
272+
273+ @property
274+ def root(self):
275+ """See `ParseApacheLogs`."""
276+ return config.librarianlogparser.logs_root
277+
278+ def getDownloadKey(self, path):
279+ """See `ParseApacheLogs`."""
280+ return get_library_file_id(path)
281+
282+ def getDownloadCountUpdater(self, file_id):
283+ """See `ParseApacheLogs`."""
284+ try:
285+ return self.libraryfilealias_set[file_id].updateDownloadCount
286+ except SQLObjectNotFound:
287+ # This file has been deleted from the librarian, so don't
288+ # try to store download counters for it.
289+ return None
290
291
292 if __name__ == '__main__':
293
294=== modified file 'cronscripts/reclaimbranchspace.py'
295--- cronscripts/reclaimbranchspace.py 2009-07-17 02:25:09 +0000
296+++ cronscripts/reclaimbranchspace.py 2009-09-03 20:06:45 +0000
297@@ -26,7 +26,7 @@
298 def main(self):
299 globalErrorUtility.configure('reclaimbranchspace')
300 job_source = getUtility(IReclaimBranchSpaceJobSource)
301- runner = JobRunner.fromReady(job_source)
302+ runner = JobRunner.fromReady(job_source, self.logger)
303 runner.runAll()
304 self.logger.info(
305 'Reclaimed space for %s branches.', len(runner.completed_jobs))
306
307=== modified file 'cronscripts/rosetta-branches.py'
308--- cronscripts/rosetta-branches.py 2009-06-24 20:52:01 +0000
309+++ cronscripts/rosetta-branches.py 2009-09-03 20:29:25 +0000
310@@ -29,7 +29,8 @@
311
312 def main(self):
313 globalErrorUtility.configure('rosettabranches')
314- runner = JobRunner.fromReady(getUtility(IRosettaUploadJobSource))
315+ runner = JobRunner.fromReady(
316+ getUtility(IRosettaUploadJobSource), self.logger)
317 server = get_scanner_server()
318 server.setUp()
319 try:
320
321=== modified file 'cronscripts/sendbranchmail.py'
322--- cronscripts/sendbranchmail.py 2009-06-24 20:52:01 +0000
323+++ cronscripts/sendbranchmail.py 2009-09-03 19:25:57 +0000
324@@ -31,7 +31,7 @@
325 globalErrorUtility.configure('sendbranchmail')
326 jobs = list(getUtility(IRevisionMailJobSource).iterReady())
327 jobs.extend(getUtility(IRevisionsAddedJobSource).iterReady())
328- runner = JobRunner(jobs)
329+ runner = JobRunner(jobs, self.logger)
330 server = get_scanner_server()
331 server.setUp()
332 try:
333
334=== added file 'cronscripts/update_preview_diffs.py'
335--- cronscripts/update_preview_diffs.py 1970-01-01 00:00:00 +0000
336+++ cronscripts/update_preview_diffs.py 2009-09-01 19:00:46 +0000
337@@ -0,0 +1,34 @@
338+#!/usr/bin/python2.4
339+#
340+# Copyright 2009 Canonical Ltd. This software is licensed under the
341+# GNU Affero General Public License version 3 (see the file LICENSE).
342+
343+# pylint: disable-msg=W0403
344+
345+"""Update or create previews diffs for branch merge proposals."""
346+
347+__metaclass__ = type
348+
349+import _pythonpath
350+
351+from lp.codehosting.vfs import get_scanner_server
352+from lp.services.job.runner import JobCronScript
353+from lp.code.interfaces.branchmergeproposal import (
354+ IUpdatePreviewDiffJobSource,)
355+
356+
357+class RunUpdatePreviewDiffJobs(JobCronScript):
358+ """Run UpdatePreviewDiff jobs."""
359+
360+ config_name = 'update_preview_diffs'
361+ source_interface = IUpdatePreviewDiffJobSource
362+
363+ def setUp(self):
364+ server = get_scanner_server()
365+ server.setUp()
366+ return [server.tearDown]
367+
368+
369+if __name__ == '__main__':
370+ script = RunUpdatePreviewDiffJobs()
371+ script.lock_and_run()
372
373=== modified file 'database/schema/security.cfg'
374--- database/schema/security.cfg 2009-08-29 19:37:54 +0000
375+++ database/schema/security.cfg 2009-09-02 19:06:19 +0000
376@@ -579,6 +579,7 @@
377 public.branch = SELECT, UPDATE
378 public.branchjob = SELECT, INSERT, UPDATE, DELETE
379 public.branchmergeproposal = SELECT, UPDATE
380+public.branchmergeproposaljob = SELECT, INSERT
381 public.branchrevision = SELECT, INSERT, UPDATE, DELETE
382 public.branchsubscription = SELECT
383 public.branchvisibilitypolicy = SELECT
384@@ -1590,6 +1591,18 @@
385 public.teamparticipation = SELECT
386 public.validpersoncache = SELECT
387
388+[update-preview-diffs]
389+type=user
390+groups=script
391+public.branch = SELECT
392+public.branchmergeproposal = SELECT, UPDATE
393+public.branchmergeproposaljob = SELECT
394+public.diff = SELECT, INSERT
395+public.job = SELECT, UPDATE
396+public.libraryfilealias = SELECT, INSERT
397+public.libraryfilecontent = SELECT, INSERT
398+public.previewdiff = SELECT, INSERT
399+
400 [send-branch-mail]
401 type=user
402 groups=script
403
404=== modified file 'lib/canonical/config/schema-lazr.conf'
405--- lib/canonical/config/schema-lazr.conf 2009-08-29 19:37:54 +0000
406+++ lib/canonical/config/schema-lazr.conf 2009-09-14 19:32:10 +0000
407@@ -1144,6 +1144,8 @@
408 # datatype: urlbase
409 restricted_download_url: http://restricted-librarian.launchpad.net/
410
411+use_https = True
412+
413 [librarian_gc]
414 # The database user which will be used by this process.
415 # datatype: string
416@@ -1346,6 +1348,14 @@
417 comments_list_max_length: 100
418 comments_list_truncate_to: 80
419
420+# Should +filebug be disabled for Ubuntu ?
421+ubuntu_disable_filebug: false
422+
423+# Redirect to this URL when users try to file a bug on Ubuntu without
424+# using apport.
425+ubuntu_bug_filing_url: https://help.ubuntu.com/community/ReportingBugs
426+
427+
428 [mpcreationjobs]
429 # The database user which will be used by this process.
430 # datatype: string
431@@ -1362,6 +1372,18 @@
432 # See [error_reports].
433 copy_to_zlog: false
434
435+[update_preview_diffs]
436+dbuser: update-preview-diffs
437+
438+# See [error_reports].
439+error_dir: none
440+
441+# See [error_reports].
442+oops_prefix: none
443+
444+# See [error_reports].
445+copy_to_zlog: false
446+
447 [person_notification]
448 # User for person notification db access
449 # datatype: string
450@@ -1409,6 +1431,10 @@
451 storm_cache: generational
452 storm_cache_size: 500
453
454+# Statement timeout (in seconds), limited to super-fast-imports query.
455+# Set to 'timeout' to make it timeout every time (for tests).
456+statement_timeout: 300
457+
458 [processmail]
459 # The database user which will be used by this process.
460 # datatype: string
461
462=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
463--- lib/canonical/launchpad/browser/launchpad.py 2009-08-28 06:38:41 +0000
464+++ lib/canonical/launchpad/browser/launchpad.py 2009-09-16 19:56:45 +0000
465@@ -9,12 +9,11 @@
466 'ApplicationButtons',
467 'BrowserWindowDimensions',
468 'DoesNotExistView',
469- 'get_launchpad_views',
470 'Hierarchy',
471 'IcingContribFolder',
472 'IcingFolder',
473+ 'LaunchpadImageFolder',
474 'LaunchpadRootNavigation',
475- 'LaunchpadImageFolder',
476 'LinkView',
477 'LoginStatus',
478 'MaintenanceMessage',
479@@ -23,6 +22,7 @@
480 'SoftTimeoutView',
481 'StructuralHeaderPresentation',
482 'StructuralObjectPresentation',
483+ 'get_launchpad_views',
484 ]
485
486
487@@ -34,6 +34,7 @@
488 import urllib
489 from datetime import timedelta, datetime
490
491+from zope.app import zapi
492 from zope.datetime import parseDatetimetz, tzinfo, DateTimeError
493 from zope.component import getUtility, queryAdapter
494 from zope.interface import implements
495@@ -95,6 +96,7 @@
496 LaunchpadFormView, LaunchpadView, Link, Navigation,
497 StandardLaunchpadFacets, canonical_name, canonical_url, custom_widget,
498 stepto)
499+from canonical.launchpad.webapp.breadcrumb import Breadcrumb
500 from canonical.launchpad.webapp.interfaces import (
501 IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu,
502 NotFoundError, POSTToNonCanonicalURL)
503@@ -233,25 +235,48 @@
504 breadcrumbs.append(breadcrumb)
505
506 host = URI(self.request.getURL()).host
507- if (len(breadcrumbs) == 0
508- or host == allvhosts.configs['mainsite'].hostname):
509- return breadcrumbs
510-
511- # If we got this far it means we have breadcrumbs and we're not on the
512- # mainsite, so we'll sneak an extra breadcrumb for the vhost we're on.
513- vhost = host.split('.')[0]
514-
515- # Iterate over the context of our breadcrumbs in reverse order and for
516- # the first one we find an adapter named after the vhost we're on,
517- # generate an extra breadcrumb and insert it in our list.
518- for idx, breadcrumb in reversed(list(enumerate(breadcrumbs))):
519- extra_breadcrumb = queryAdapter(
520- breadcrumb.context, IBreadcrumb, name=vhost)
521- if extra_breadcrumb is not None:
522- breadcrumbs.insert(idx + 1, extra_breadcrumb)
523- break
524+ mainhost = allvhosts.configs['mainsite'].hostname
525+ if len(breadcrumbs) != 0 and host != mainhost:
526+ # We have breadcrumbs and we're not on the mainsite, so we'll
527+ # sneak an extra breadcrumb for the vhost we're on.
528+ vhost = host.split('.')[0]
529+
530+ # Iterate over the context of our breadcrumbs in reverse order and
531+ # for the first one we find an adapter named after the vhost we're
532+ # on, generate an extra breadcrumb and insert it in our list.
533+ for idx, breadcrumb in reversed(list(enumerate(breadcrumbs))):
534+ extra_breadcrumb = queryAdapter(
535+ breadcrumb.context, IBreadcrumb, name=vhost)
536+ if extra_breadcrumb is not None:
537+ breadcrumbs.insert(idx + 1, extra_breadcrumb)
538+ break
539+ if len(breadcrumbs) > 0:
540+ page_crumb = self.makeBreadcrumbForRequestedPage()
541+ if page_crumb:
542+ breadcrumbs.append(page_crumb)
543 return breadcrumbs
544
545+ def makeBreadcrumbForRequestedPage(self):
546+ """Return an `IBreadcrumb` for the requested page.
547+
548+ The `IBreadcrumb` for the requested page is created using the current
549+ URL and the page's name (i.e. the last path segment of the URL).
550+
551+ If the requested page (as specified in self.request) is the default
552+ one for the last traversed object, return None.
553+ """
554+ url = self.request.getURL()
555+ last_segment = URI(url).path.split('/')[-1]
556+ default_view_name = zapi.getDefaultViewName(
557+ self.request.traversed_objects[-1], self.request)
558+ if last_segment.startswith('+') and last_segment != default_view_name:
559+ breadcrumb = Breadcrumb(None)
560+ breadcrumb._url = url
561+ breadcrumb.text = last_segment
562+ return breadcrumb
563+ else:
564+ return None
565+
566 @property
567 def display_breadcrumbs(self):
568 """Return whether the breadcrumbs should be displayed."""
569@@ -259,6 +284,7 @@
570 # to display it as it will simply repeat the context.title.
571 return len(self.items) > 1
572
573+
574 class MaintenanceMessage:
575 """Display a maintenance message if the control file is present and
576 it contains a valid iso format time.
577
578=== modified file 'lib/canonical/launchpad/browser/logintoken.py'
579--- lib/canonical/launchpad/browser/logintoken.py 2009-07-20 15:27:26 +0000
580+++ lib/canonical/launchpad/browser/logintoken.py 2009-09-18 01:09:10 +0000
581@@ -93,6 +93,8 @@
582 }
583 login_token_pages.update(auth_token_pages)
584 PAGES = login_token_pages
585+ page_title = 'You have already done this'
586+ label = 'Confirmation already concluded'
587
588 def render(self):
589 if self.context.date_consumed is None:
590@@ -109,6 +111,11 @@
591 expected_token_types = ()
592 successfullyProcessed = False
593
594+ @property
595+ def page_title(self):
596+ """The page title."""
597+ return self.label
598+
599 def redirectIfInvalidOrConsumedToken(self):
600 """If this is a consumed or invalid token redirect to the LoginToken
601 default view and return True.
602@@ -354,6 +361,15 @@
603 expected_token_types = (LoginTokenType.VALIDATEGPG,
604 LoginTokenType.VALIDATESIGNONLYGPG)
605
606+ @property
607+ def label(self):
608+ if self.context.tokentype == LoginTokenType.VALIDATESIGNONLYGPG:
609+ return 'Confirm sign-only OpenPGP key'
610+ else:
611+ assert self.context.tokentype == LoginTokenType.VALIDATEGPG, (
612+ 'unexpected token type: %r' % self.context.tokentype)
613+ return 'Confirm OpenPGP key'
614+
615 def initialize(self):
616 if not self.redirectIfInvalidOrConsumedToken():
617 if self.context.tokentype == LoginTokenType.VALIDATESIGNONLYGPG:
618@@ -571,6 +587,7 @@
619 schema = Interface
620 field_names = []
621 expected_token_types = (LoginTokenType.VALIDATEEMAIL,)
622+ label = 'Confirm e-mail address'
623
624 def initialize(self):
625 self.redirectIfInvalidOrConsumedToken()
626@@ -670,6 +687,7 @@
627 class ValidateTeamEmailView(ValidateEmailView):
628
629 expected_token_types = (LoginTokenType.VALIDATETEAMEMAIL,)
630+ # The desired label is the same as ValidateEmailView.
631
632 def markEmailAsValid(self, email):
633 """See `ValidateEmailView`"""
634@@ -679,6 +697,7 @@
635 class MergePeopleView(BaseTokenView, LaunchpadView):
636 expected_token_types = (LoginTokenType.ACCOUNTMERGE,)
637 mergeCompleted = False
638+ label = 'Merge Launchpad accounts'
639
640 def initialize(self):
641 self.redirectIfInvalidOrConsumedToken()
642
643=== modified file 'lib/canonical/launchpad/browser/packaging.py'
644--- lib/canonical/launchpad/browser/packaging.py 2009-06-25 05:30:52 +0000
645+++ lib/canonical/launchpad/browser/packaging.py 2009-09-12 06:11:08 +0000
646@@ -18,9 +18,15 @@
647
648 class PackagingAddView(LaunchpadFormView):
649 schema = IPackaging
650- label = 'Add distribution packaging record'
651 field_names = ['distroseries', 'sourcepackagename', 'packaging']
652
653+ @property
654+ def label(self):
655+ """See `LaunchpadFormView`."""
656+ return 'Packaging of %s in distributions' % self.context.displayname
657+
658+ page_title = label
659+
660 def validate(self, data):
661 productseries = self.context
662 sourcepackagename = data['sourcepackagename']
663
664=== modified file 'lib/canonical/launchpad/browser/structuralsubscription.py'
665--- lib/canonical/launchpad/browser/structuralsubscription.py 2009-08-24 01:09:07 +0000
666+++ lib/canonical/launchpad/browser/structuralsubscription.py 2009-09-17 17:12:58 +0000
667@@ -34,6 +34,16 @@
668 custom_widget('subscriptions_team', LabeledMultiCheckBoxWidget)
669 custom_widget('remove_other_subscriptions', LabeledMultiCheckBoxWidget)
670
671+ override_title_breadcrumbs = True
672+
673+ @property
674+ def page_title(self):
675+ return 'Subscribe to Bugs in %s' % self.context.title
676+
677+ @property
678+ def label(self):
679+ return self.page_title
680+
681 def setUpFields(self):
682 """See LaunchpadFormView."""
683 LaunchpadFormView.setUpFields(self)
684@@ -167,7 +177,7 @@
685 'e-mail each time someone reports or changes one of '
686 'its public bugs.' % target.displayname)
687 elif is_subscribed and not subscribe:
688- target.removeBugSubscription(self.user)
689+ target.removeBugSubscription(self.user, self.user)
690 self.request.response.addNotification(
691 'You have unsubscribed from "%s". You '
692 'will no longer automatically receive e-mail about '
693@@ -197,7 +207,7 @@
694 team.displayname, self.context.displayname))
695
696 for team in subscriptions - form_selected_teams:
697- target.removeBugSubscription(team)
698+ target.removeBugSubscription(team, self.user)
699 self.request.response.addNotification(
700 'The %s team will no longer automatically receive '
701 'e-mail about changes to public bugs in "%s".' % (
702@@ -220,7 +230,7 @@
703
704 subscriptions_to_remove = data.get('remove_other_subscriptions', [])
705 for subscription in subscriptions_to_remove:
706- target.removeBugSubscription(subscription)
707+ target.removeBugSubscription(subscription, self.user)
708 self.request.response.addNotification(
709 '%s will no longer automatically receive e-mail about '
710 'public bugs in "%s".' % (
711
712=== modified file 'lib/canonical/launchpad/database/librarian.py'
713--- lib/canonical/launchpad/database/librarian.py 2009-08-27 01:32:01 +0000
714+++ lib/canonical/launchpad/database/librarian.py 2009-09-14 19:32:10 +0000
715@@ -98,7 +98,7 @@
716
717 def getURL(self):
718 """See ILibraryFileAlias.getURL"""
719- if config.vhosts.use_https:
720+ if config.librarian.use_https:
721 return self.https_url
722 else:
723 return self.http_url
724
725=== modified file 'lib/canonical/launchpad/database/structuralsubscription.py'
726--- lib/canonical/launchpad/database/structuralsubscription.py 2009-07-23 13:44:13 +0000
727+++ lib/canonical/launchpad/database/structuralsubscription.py 2009-08-25 12:04:58 +0000
728@@ -5,6 +5,7 @@
729 __all__ = ['StructuralSubscription',
730 'StructuralSubscriptionTargetMixin']
731
732+from zope.component import getUtility
733 from zope.interface import implements
734
735 from sqlobject import ForeignKey
736@@ -16,9 +17,10 @@
737
738 from canonical.launchpad.interfaces import (
739 BlueprintNotificationLevel, BugNotificationLevel, DeleteSubscriptionError,
740- IDistribution, IDistributionSourcePackage, IDistroSeries, IMilestone,
741- IProduct, IProductSeries, IProject, IStructuralSubscription,
742- IStructuralSubscriptionTarget)
743+ IDistribution, IDistributionSourcePackage, IDistroSeries,
744+ ILaunchpadCelebrities, IMilestone, IProduct, IProductSeries, IProject,
745+ IStructuralSubscription, IStructuralSubscriptionTarget,
746+ UserCannotSubscribePerson)
747 from lp.registry.interfaces.person import (
748 validate_public_person, validate_person_not_private_membership)
749
750@@ -129,8 +131,35 @@
751 '%s is not a valid structural subscription target.')
752 return args
753
754+ def _userCanAlterSubscription(self, subscriber, subscribed_by):
755+ """Check if a user can change a subscription for a person."""
756+ # A Launchpad administrator or the user can subscribe a user.
757+ # A Launchpad or team admin can subscribe a team.
758+
759+ # Nobody else can, unless the context is a IDistributionSourcePackage,
760+ # in which case the drivers or owner can.
761+ if IDistributionSourcePackage.providedBy(self):
762+ for driver in self.distribution.drivers:
763+ if subscribed_by.inTeam(driver):
764+ return True
765+ if subscribed_by.inTeam(self.distribution.owner):
766+ return True
767+
768+ admins = getUtility(ILaunchpadCelebrities).admin
769+ return (subscriber is subscribed_by or
770+ subscriber in subscribed_by.getAdministratedTeams() or
771+ subscribed_by.inTeam(admins))
772+
773 def addSubscription(self, subscriber, subscribed_by):
774 """See `IStructuralSubscriptionTarget`."""
775+ if subscriber is None:
776+ subscriber = subscribed_by
777+
778+ if not self._userCanAlterSubscription(subscriber, subscribed_by):
779+ raise UserCannotSubscribePerson(
780+ '%s does not have permission to subscribe %s.' % (
781+ subscribed_by.name, subscriber.name))
782+
783 existing_subscription = self.getSubscription(subscriber)
784
785 if existing_subscription is not None:
786@@ -151,20 +180,28 @@
787 sub.bug_notification_level = BugNotificationLevel.COMMENTS
788 return sub
789
790- def removeBugSubscription(self, person):
791+ def removeBugSubscription(self, subscriber, unsubscribed_by):
792 """See `IStructuralSubscriptionTarget`."""
793+ if subscriber is None:
794+ subscriber = unsubscribed_by
795+
796+ if not self._userCanAlterSubscription(subscriber, unsubscribed_by):
797+ raise UserCannotSubscribePerson(
798+ '%s does not have permission to unsubscribe %s.' % (
799+ unsubscribed_by.name, subscriber.name))
800+
801 subscription_to_remove = None
802 for subscription in self.getSubscriptions(
803 min_bug_notification_level=BugNotificationLevel.METADATA):
804 # Only search for bug subscriptions
805- if subscription.subscriber == person:
806+ if subscription.subscriber == subscriber:
807 subscription_to_remove = subscription
808 break
809
810 if subscription_to_remove is None:
811 raise DeleteSubscriptionError(
812 "%s is not subscribed to %s." % (
813- person.name, self.displayname))
814+ subscriber.name, self.displayname))
815 else:
816 if (subscription_to_remove.blueprint_notification_level >
817 BlueprintNotificationLevel.NOTHING):
818
819=== modified file 'lib/canonical/launchpad/doc/canonical_url_examples.txt'
820--- lib/canonical/launchpad/doc/canonical_url_examples.txt 2009-08-25 22:27:24 +0000
821+++ lib/canonical/launchpad/doc/canonical_url_examples.txt 2009-09-10 10:27:20 +0000
822@@ -22,7 +22,7 @@
823
824 >>> from canonical.launchpad.interfaces import (
825 ... IMaloneApplication, IBazaarApplication,
826- ... IRosettaApplication, ILaunchpadRoot, IQuestionSet
827+ ... ILaunchpadRoot, IQuestionSet
828 ... )
829
830 The Launchpad homepage.
831@@ -35,11 +35,6 @@
832 >>> canonical_url(getUtility(IMaloneApplication))
833 u'http://launchpad.dev/bugs'
834
835-The Rosetta homepage.
836-
837- >>> canonical_url(getUtility(IRosettaApplication))
838- u'http://launchpad.dev/translations'
839-
840 The Bazaar homepage.
841
842 >>> canonical_url(getUtility(IBazaarApplication))
843@@ -56,6 +51,9 @@
844 >>> canonical_url(getUtility(IMailingListSet))
845 u'http://launchpad.dev/+mailinglists'
846
847+Launchpad Translations (Rosetta) canonical_url examples are in
848+lib/lp/translations/doc/canonical_url_examples.txt.
849+
850
851 == Persons and Teams ==
852
853@@ -300,20 +298,20 @@
854 An IBugTrackerSet.
855
856 >>> canonical_url(getUtility(IBugTrackerSet))
857- u'http://launchpad.dev/bugs/bugtrackers'
858+ u'http://bugs.launchpad.dev/bugs/bugtrackers'
859
860 A remote bug tracker.
861
862 >>> mozilla_bugtracker = getUtility(IBugTrackerSet)['mozilla.org']
863 >>> canonical_url(mozilla_bugtracker)
864- u'http://launchpad.dev/bugs/bugtrackers/mozilla.org'
865+ u'http://bugs.launchpad.dev/bugs/bugtrackers/mozilla.org'
866
867 A bug from a remote bug tracker.
868
869 >>> remote_bug = RemoteBug(mozilla_bugtracker, '42',
870 ... mozilla_bugtracker.getBugsWatching('42'))
871 >>> canonical_url(remote_bug)
872- u'http://launchpad.dev/bugs/bugtrackers/mozilla.org/42'
873+ u'http://bugs.launchpad.dev/bugs/bugtrackers/mozilla.org/42'
874
875
876 == Branches ==
877@@ -363,97 +361,6 @@
878 http://code.launchpad.dev/~name12/gnome-terminal/main/+merge/.../comments/...
879
880
881-== POTemplates and so on ==
882-
883- >>> from lp.translations.interfaces.potemplate import IPOTemplateSet
884- >>> from lp.translations.interfaces.translationgroup import (
885- ... ITranslationGroupSet)
886-
887-Most Rosetta pages hang off IPOTemplateSubset objects, of which there are two
888-varieties: distribution and upstream.
889-
890-First, the distribution kind. We'll need the source package name.
891-
892- >>> sourcepackagename = sourcepackagenameset['evolution']
893-
894-And here's our subset.
895-
896- >>> potemplateset = getUtility(IPOTemplateSet)
897- >>> potemplatesubset = potemplateset.getSubset(
898- ... distroseries=hoary, sourcepackagename=sourcepackagename)
899-
900- >>> canonical_url(potemplatesubset)
901- u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots'
902-
903-We can get a particular PO template for this source package by its PO template
904-name.
905-
906- >>> potemplate = potemplatesubset['evolution-2.2']
907- >>> canonical_url(potemplate)
908- u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2'
909-
910-And we can get a particular PO file for this PO template by its language code.
911-
912- >>> pofile = potemplate.getPOFileByLang('es')
913- >>> canonical_url(pofile)
914- u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es'
915-
916-Also, we can get the url to a translation message.
917-
918- >>> potmsgset = potemplate.getPOTMsgSetBySequence(1)
919- >>> translationmessage = potmsgset.getCurrentTranslationMessage(
920- ... pofile.potemplate, pofile.language)
921- >>> translationmessage.setPOFile(pofile)
922- >>> print canonical_url(translationmessage)
923- http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/1
924-
925-Even for a dummy one.
926-
927- >>> potmsgset = potemplate.getPOTMsgSetBySequence(20)
928- >>> translationmessage = potmsgset.getCurrentDummyTranslationMessage(
929- ... pofile.potemplate, pofile.language)
930- >>> print canonical_url(translationmessage)
931- http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/20
932-
933-Upstream POTemplateSubsets work in much the same way, except they hang off a
934-product series. Let's get a product series.
935-
936-Now we can get an upstream subset and do the same sorts of thing as we did
937-with the distro subset.
938-
939- >>> potemplatesubset = potemplateset.getSubset(
940- ... productseries=evolution_trunk_series)
941- >>> potemplate = potemplatesubset['evolution-2.2']
942- >>> canonical_url(potemplate)
943- u'http://launchpad.dev/evolution/trunk/+pots/evolution-2.2'
944-
945- >>> pofile = potemplate.getPOFileByLang('es')
946- >>> canonical_url(pofile)
947- u'http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es'
948-
949-Also, we can get the url to a dummy one
950-
951- >>> potmsgset = potemplate.getPOTMsgSetBySequence(1)
952- >>> translationmessage = potmsgset.getCurrentTranslationMessage(
953- ... pofile.potemplate, pofile.language)
954- >>> translationmessage.setPOFile(pofile)
955- >>> print canonical_url(translationmessage)
956- http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/1
957-
958-Even for a dummy PO msgset
959-
960- >>> potmsgset = potemplate.getPOTMsgSetBySequence(20)
961- >>> translationmessage = potmsgset.getCurrentDummyTranslationMessage(
962- ... pofile.potemplate, pofile.language)
963- >>> print canonical_url(translationmessage)
964- http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/20
965-
966-Rosetta also has translation groups.
967-
968- >>> canonical_url(getUtility(ITranslationGroupSet))
969- u'http://translations.launchpad.dev/+groups'
970-
971-
972 == Specifications ==
973
974 >>> from canonical.launchpad.interfaces import ISpecificationSet
975
976=== modified file 'lib/canonical/launchpad/doc/distribution-soyuz.txt'
977--- lib/canonical/launchpad/doc/distribution-soyuz.txt 2009-08-14 12:59:56 +0000
978+++ lib/canonical/launchpad/doc/distribution-soyuz.txt 2009-08-30 23:57:41 +0000
979@@ -13,9 +13,10 @@
980 distribution:
981
982 >>> from lp.registry.interfaces.distribution import IDistributionSet
983+ >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
984 >>> from canonical.launchpad.interfaces import (
985 ... ISourcePackageName, IBinaryPackageName,
986- ... PackagePublishingPocket, PackagePublishingStatus)
987+ ... PackagePublishingStatus)
988
989 >>> distroset = getUtility(IDistributionSet)
990 >>> gentoo = distroset.getByName("gentoo")
991
992=== modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt'
993--- lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-26 09:33:33 +0000
994+++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-09-17 20:04:28 +0000
995@@ -170,46 +170,6 @@
996 url='http://launchpad.dev/joy-of-cooking'
997 text='Joy of cooking'>
998
999-Breadcrumbs may have icons. The icon is only set for a breadcrumb if
1000-the builder's context has an IPathAdapter registration.
1001-
1002- >>> from zope.traversing.interfaces import IPathAdapter
1003- >>> from canonical.launchpad.webapp.tales import (
1004- ... ObjectImageDisplayAPI)
1005-
1006- # We need a custom image display adapter that overrides the
1007- # the icon() method and returns an <img> tag.
1008- >>> class RecipeImageDisplayAPI(ObjectImageDisplayAPI):
1009- ... def icon(self):
1010- ... return '<img src="/@@/recipe"/>'
1011-
1012- >>> provideAdapter(
1013- ... RecipeImageDisplayAPI, [IRecipe], IPathAdapter, 'image')
1014-
1015- >>> breadcrumb = DynamicBreadcrumb(recipe)
1016- >>> breadcrumb
1017- <DynamicBreadcrumb
1018- url='http://launchpad.dev/joy-of-cooking/spam'
1019- text='Spam'
1020- icon='<img src="/@@/recipe"/>'>
1021-
1022-The icon is not set if the default image adapter can not find an
1023-icon for the object.
1024-
1025- # We'll use the default image adapter, which doesn't know about
1026- # ICookbook objects.
1027- >>> provideAdapter(
1028- ... ObjectImageDisplayAPI, [ICookbook], IPathAdapter, 'image')
1029-
1030- >>> print queryAdapter(cookbook, IPathAdapter, name='image').icon()
1031- None
1032-
1033- >>> breadcrumb = DynamicBreadcrumb(cookbook)
1034- >>> breadcrumb
1035- <DynamicBreadcrumb
1036- url='http://launchpad.dev/joy-of-cooking'
1037- text='Joy of cooking'>
1038-
1039
1040 == Customizing the hierarchy ==
1041
1042@@ -230,8 +190,7 @@
1043 >>> spammy_hierarchy.items
1044 [<TextualBreadcrumb
1045 url='http://launchpad.dev/joy-of-cooking/spam'
1046- text='Spam'
1047- icon='<img src="/@@/recipe"/>'>]
1048+ text='Spam'>]
1049
1050
1051 == Rendering the list ==
1052@@ -267,26 +226,3 @@
1053
1054 >>> print_hierarchy(homepage_hierarchy.render())
1055 Location:
1056-
1057-Breadcrumbs in the hierarchy that have icons are rendered with an <img>
1058-tag. Breadcrumbs without icons are not.
1059-
1060- >>> breadcrumb_no_icon, breadcrumb_with_icon = hierarchy.items
1061-
1062- >>> breadcrumb_no_icon
1063- <TextualBreadcrumb
1064- url='http://launchpad.dev/joy-of-cooking'
1065- text='Joy of cooking'>
1066-
1067- >>> breadcrumb_with_icon
1068- <TextualBreadcrumb
1069- url='http://launchpad.dev/joy-of-cooking/spam'
1070- text='Spam'
1071- icon='<img src="/@@/recipe"/>'>
1072-
1073- >>> soup = BeautifulSoup(hierarchy.render())
1074- >>> img_elems = soup.findAll('img')
1075- >>> for img in img_elems:
1076- ... print img
1077- <img src="/@@/recipe" />
1078-
1079
1080=== modified file 'lib/canonical/launchpad/doc/launchbag.txt'
1081--- lib/canonical/launchpad/doc/launchbag.txt 2009-05-12 08:10:20 +0000
1082+++ lib/canonical/launchpad/doc/launchbag.txt 2009-09-03 00:08:57 +0000
1083@@ -57,6 +57,11 @@
1084 >>> print launchbag.login
1085 None
1086
1087+'user' will also be set to None:
1088+
1089+>>> print launchbag.user
1090+None
1091+
1092 Let's do a cookie auth principal identification. In this case, the login
1093 will be cookie@example.com.
1094
1095
1096=== modified file 'lib/canonical/launchpad/doc/launchpadview.txt'
1097--- lib/canonical/launchpad/doc/launchpadview.txt 2009-04-17 10:32:16 +0000
1098+++ lib/canonical/launchpad/doc/launchpadview.txt 2009-09-16 08:08:56 +0000
1099@@ -101,3 +101,13 @@
1100 >>> view.error_message = structured('Information overload.')
1101 >>> view.error_message.escapedtext
1102 u'Information overload.'
1103+
1104+Every Launchpad view also knows whether edge redirection has been inhibited.
1105+
1106+ >>> view.isRedirectInhibited()
1107+ False
1108+ >>> new_request = TestRequest(HTTP_COOKIE="inhibit_beta_redirect=1")
1109+ >>> view = MyView(context, new_request)
1110+ >>> view.isRedirectInhibited()
1111+ True
1112+
1113
1114=== modified file 'lib/canonical/launchpad/doc/librarian.txt'
1115--- lib/canonical/launchpad/doc/librarian.txt 2009-08-07 12:54:05 +0000
1116+++ lib/canonical/launchpad/doc/librarian.txt 2009-09-15 02:18:25 +0000
1117@@ -78,7 +78,7 @@
1118
1119 >>> from textwrap import dedent
1120 >>> test_data = dedent("""
1121- ... [vhosts]
1122+ ... [librarian]
1123 ... use_https: true
1124 ... """)
1125 >>> config.push('test', test_data)
1126@@ -689,8 +689,8 @@
1127
1128 == Time to last download ==
1129
1130-The .last_downloaded property gives us the time delta from today to the day
1131-that file was last downloaded, or None if it's never been downloaded.
1132+The .last_downloaded property gives us the time delta from today to the day
1133+that file was last downloaded, or None if it's never been downloaded.
1134
1135 >>> today = datetime.now(utc).date()
1136 >>> public_file.last_downloaded == today - last_downloaded_date
1137
1138=== modified file 'lib/canonical/launchpad/doc/structural-subscriptions.txt'
1139--- lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-04-17 10:32:16 +0000
1140+++ lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-08-25 11:21:05 +0000
1141@@ -160,8 +160,8 @@
1142
1143 >>> evolution_sub.blueprint_notification_level = (
1144 ... BlueprintNotificationLevel.METADATA)
1145- >>> evolution_package.removeBugSubscription(sampleperson)
1146- >>> ubuntu.removeBugSubscription(sampleperson)
1147+ >>> evolution_package.removeBugSubscription(sampleperson, sampleperson)
1148+ >>> ubuntu.removeBugSubscription(sampleperson, sampleperson)
1149 >>> syncUpdate(evolution_sub)
1150
1151 Sample Person is no longer a subscriber to the package, but Foo Bar
1152
1153=== modified file 'lib/canonical/launchpad/doc/tales.txt'
1154--- lib/canonical/launchpad/doc/tales.txt 2009-08-20 12:24:29 +0000
1155+++ lib/canonical/launchpad/doc/tales.txt 2009-09-10 10:49:44 +0000
1156@@ -214,6 +214,19 @@
1157 >>> test_tales('foo/fmt:shorten/8', foo='abcdefghij')
1158 'abcde...'
1159
1160+To ellipsize the middle of a string. use fmt:ellipsize and pass the max
1161+length.
1162+
1163+ >>> print test_tales('foo/fmt:ellipsize/25',
1164+ ... foo='foo-bar-baz-bazoo_22.443.tar.gz')
1165+ foo-bar-baz....443.tar.gz
1166+
1167+The string is not ellipsized if it is less than the max length.
1168+
1169+ >>> print test_tales('foo/fmt:ellipsize/25',
1170+ ... foo='firefox-0.9.2.orig.tar.gz')
1171+ firefox-0.9.2.orig.tar.gz
1172+
1173 To preserve newlines in text when displaying as HTML, use fmt:nl_to_br:
1174
1175 >>> test_tales('foo/fmt:nl_to_br',
1176@@ -333,6 +346,8 @@
1177 * blueprint-branch links
1178 * projects
1179 * questions
1180+ * distributions
1181+ * distroseries
1182
1183
1184 Person entries
1185@@ -634,6 +649,22 @@
1186 u'<a... class="sprite question">1:...</a>'
1187
1188
1189+Distributions
1190+.............
1191+
1192+ >>> distribution = factory.makeDistribution()
1193+ >>> test_tales("distribution/fmt:link", distribution=distribution)
1194+ u'<a... class="sprite distribution">...</a>'
1195+
1196+
1197+Distribution Series
1198+...................
1199+
1200+ >>> distroseries = factory.makeDistroArchSeries().distroseries
1201+ >>> test_tales("distroseries/fmt:link", distroseries=distroseries)
1202+ u'<a href="...">...</a>'
1203+
1204+
1205 The fmt: namespace for specially formatted object info
1206 ------------------------------------------------------
1207
1208@@ -650,7 +681,7 @@
1209 The "standard" 'url' name is supported:
1210
1211 >>> test_tales("bugtracker/fmt:url", bugtracker=bugtracker)
1212- u'/bugs/bugtrackers/email'
1213+ u'http://bugs.launchpad.dev/bugs/bugtrackers/email'
1214
1215 (The url is relative if possible, and our test request claims to be from
1216 launchpad.dev, so the url is relative.)
1217
1218=== modified file 'lib/canonical/launchpad/icing/style-3-0.css'
1219--- lib/canonical/launchpad/icing/style-3-0.css 2009-09-01 16:19:19 +0000
1220+++ lib/canonical/launchpad/icing/style-3-0.css 2009-09-18 03:48:00 +0000
1221@@ -7,7 +7,7 @@
1222 #maincontent {
1223 float: left;
1224 width: 100%;
1225- margin-right: -25em;
1226+ margin-right: -25%;
1227 }
1228 #maincontent ol, #maincontent ul {
1229 padding-left: auto;
1230@@ -207,7 +207,7 @@
1231 }
1232 .footer {
1233 clear: both;
1234- margin-top: 2em;
1235+ margin-top: 4em;
1236 padding-top: 0.5em;
1237 }
1238 .footer .lp-arcana {
1239@@ -230,14 +230,20 @@
1240 }
1241 .portlet, .aside {
1242 clear: both;
1243- border-top: 1px solid #d6d6d6;
1244- padding: 0.5em 0;
1245+ border-top: 1px solid #EBEBEB;
1246+ padding: 1em 0;
1247 }
1248 .top-portlet {
1249 padding: 0 0 0.5em 0;
1250 margin: 0 0 1em;
1251 }
1252-
1253+.full-page-width {
1254+ z-index: 10;
1255+ width: 131%;
1256+ }
1257+.warning.message {
1258+ margin-top: 17px;
1259+ }
1260 /*
1261
1262 Use percentages when setting font-size.
1263@@ -397,6 +403,9 @@
1264 color: #666;
1265 font-style: italic;
1266 }
1267+li .registered {
1268+ font-style: normal;
1269+ }
1270 .description {
1271 clear: both;
1272 font-size: 100%;
1273@@ -523,6 +532,15 @@
1274 color: #747474;
1275 }
1276
1277+/* Registering slot */
1278+.registering {
1279+ float: right;
1280+ padding-top: 10px;
1281+ font-size: 85%;
1282+ color: #666;
1283+ font-style: italic;
1284+}
1285+
1286 /* Side content exceptions */
1287 .side {
1288 padding: 0.5em;
1289@@ -546,9 +564,6 @@
1290 background: #fbfbfb;
1291 }
1292
1293-.downloads a {
1294- color: #4f843c;
1295- }
1296 .downloads li {
1297 margin: 0;
1298 padding: 2px 0 0;
1299@@ -564,17 +579,20 @@
1300 border-radius: 3px;
1301 background: #4f843c url(/@@/bg-project-downloads.png) center right no-repeat;
1302 padding: 6%;
1303- padding-right: 50px;
1304+ padding-right: 40px;
1305 color: #fff;
1306- font-size: 108%;
1307- text-decoration: underline;
1308+ font-size: 93%;
1309+ }
1310+.downloads .version {
1311+ -moz-border-radius: 5px 5px 0 0;
1312+ -webkit-border-radius: 5px 5px 0 0;
1313+ -khtml-border-radius: 5px 5px 0 0;
1314+ border-radius: 5px 5px 0 0;
1315+ background: #d3e3c7;
1316+ padding: 0.2em 1em;
1317 }
1318 .downloads .released {
1319- margin: .3em 0 1em 0;
1320- padding: 0 .2em 0 0;
1321- text-align: right;
1322- }
1323-.downloads .released span {
1324+ margin: .3em 0 .5em 0;
1325 -moz-border-radius: 0 0 5px 5px;
1326 -webkit-border-radius: 0 0 5px 5px;
1327 -khtml-border-radius: 0 0 5px 5px;
1328@@ -583,7 +601,7 @@
1329 padding: 0.2em 1em;
1330 }
1331 .downloads .alternate {
1332- text-align: right;
1333+ padding: 0 0 0 1em;
1334 }
1335
1336 ul.super-add-action {
1337@@ -610,11 +628,8 @@
1338 text-decoration: underline;
1339 }
1340
1341-.involvement ul {
1342- border-top: 1px solid #d0d0d0;
1343- }
1344 .involvement li {
1345- border-bottom: 1px solid #d0d0d0;
1346+ border-top: 1px solid #d0d0d0;
1347 padding: 0;
1348 font-size: 108%;
1349 font-weight: bold;
1350@@ -643,6 +658,10 @@
1351 color: #5ba4c6;
1352 background: url(/@@/blueprints-arrow-right.png) right center no-repeat;
1353 }
1354+.involvement a:hover {
1355+ text-decoration: none;
1356+ background-color: #eee;
1357+ }
1358
1359 .announcements li {
1360 margin-bottom: 0.5em;
1361@@ -662,6 +681,39 @@
1362 margin-top: -2px;
1363 }
1364
1365+/* For the Latest updates portlet
1366+ * at https://launchpad.dev/~cprov/+archive/ppa */
1367+ul.latest-ppa-updates .duration {
1368+ font-size: 75%;
1369+}
1370+ul.latest-ppa-updates li {
1371+ padding: 3px;
1372+ background-repeat: no-repeat;
1373+ background-position:right center;
1374+}
1375+/* The following could be generalised for use to the following selector:
1376+ * .side .portlet li.nth-child(odd)
1377+ * if needed. */
1378+ul.latest-ppa-updates li:nth-child(odd) {
1379+ border-top: 1px solid #dedede;
1380+ border-bottom: 1px solid #dedede;
1381+ background-color: #eeeeff;
1382+}
1383+ul.latest-ppa-updates li.FULLYBUILT {
1384+ background-image: url('/@@/yes');
1385+}
1386+ul.latest-ppa-updates li.FULLYBUILT_PENDING {
1387+ background-image: url('/@@/build-success-publishing');
1388+}
1389+ul.latest-ppa-updates li.NEEDSBUILD {
1390+ background-image: url('/@@/build-needed');
1391+}
1392+ul.latest-ppa-updates li.FAILEDTOBUILD {
1393+ background-image: url('/@@/build-failed');
1394+}
1395+ul.latest-ppa-updates li.BUILDING {
1396+ background-image: url('/@@/processing');
1397+}
1398 /* From nice_pre in tales.py */
1399 pre.wrap {
1400 white-space: -moz-pre-wrap;
1401@@ -672,11 +724,22 @@
1402
1403 /* ==== Listing tables ==== */
1404
1405-table.listing thead {
1406- font-size: 116%;
1407-}
1408 table.listing th {
1409- font-weight: bold;
1410+ font-weight: bold;
1411+}
1412+table.narrow-listing {
1413+ width: 45em;
1414+}
1415+/* ~person/+karma */
1416+table.cozy-listing {
1417+ width: 20em;
1418+ background-color: #fff;
1419+ border: 1px solid #d2d2d2;
1420+ border-bottom: 1px solid #d2d2d2;
1421+}
1422+table.cozy-listing td {
1423+ border: 1px #d2d2d2;
1424+ border-style: dotted none none none;
1425 }
1426
1427 ul.language, li.language {
1428@@ -688,57 +751,89 @@
1429 /* ==== Translations hand-made forms ==== */
1430
1431 form.translations div.fields {
1432- padding: 1em;
1433+ padding: 1em;
1434 }
1435
1436 form.translations div.actions {
1437- padding: 1em;
1438- text-align: right;
1439+ padding: 1em;
1440+ text-align: right;
1441+ clear:both;
1442 }
1443
1444 form.translations input {
1445- padding-left: 0.5em;
1446- padding-right: 0.5em;
1447+ padding-left: 0.5em;
1448+ padding-right: 0.5em;
1449 }
1450
1451 form.translations select {
1452- margin-left: 0.5em;
1453- padding-right: 0.5em;
1454+ margin-left: 0.5em;
1455+ padding-right: 0.5em;
1456 }
1457
1458 form.translations label {
1459- padding-left: 0.5em;
1460- padding-right: 1em;
1461+ padding-left: 0.5em;
1462+ padding-right: 1em;
1463 }
1464
1465 form.translations .listbox label {
1466- padding: 2px 1em 2px 2px;
1467+ padding: 2px 1em 2px 2px;
1468 }
1469
1470 /* Provide top-alignment for radio boxes and longer explanations
1471 * without using tables.
1472+ *
1473+ * Examples:
1474+ * https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/+upload
1475+ * https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/+export
1476 */
1477-form.translations div.fields div.alignment {
1478- position: relative;
1479-}
1480-form.translations div.alignment div.top-alignment {
1481- position: absolute;
1482- top: 0px;
1483-}
1484-form.translations div.alignment .spacer {
1485- visibility: hidden;
1486-}
1487 form.translations div.alignment .content {
1488- display: inline-block;
1489- margin-left: 0px;
1490-}
1491-
1492+ float:left;
1493+}
1494+form.translations div.alignment .selector {
1495+ margin-right: 0.5em;
1496+ float: left;
1497+ clear: both;
1498+}
1499 form.translations div.alignment .content label {
1500- padding: 0px;
1501- margin: 0px;
1502- font-weight: bold;
1503-}
1504-
1505-table.narrow-listing {
1506- width: 45em;
1507+ padding: 0px;
1508+ margin: 0px;
1509+ font-weight: bold;
1510+}
1511+form.translations div.alignment .secondary label {
1512+ font-weight: normal;
1513+ padding: 2px 1em 2px 2px;
1514+}
1515+
1516+/* Translations statistics and legend.
1517+ *
1518+ * Examples:
1519+ * https://translations.launchpad.dev/ubuntu/hoary/+lang/es
1520+ * https://translations.launchpad.dev/evolution/trunk/+lang/es
1521+ */
1522+
1523+div.translations-legend {
1524+ padding-top: 2em;
1525+ padding-bottom: 1em;
1526+}
1527+table.translation-stats td {
1528+ text-align:center;
1529+}
1530+table.translation-stats td.template-name {
1531+ text-align:left;
1532+}
1533+table.translation-stats tfoot td,
1534+table.translation-stats tfoot th {
1535+ background-color: #f7f7f7;
1536+ border: 0px;
1537+ border-top: 2px solid #d2d2d2;
1538+ border-bottom: 2px solid #d2d2d2;
1539+ padding-top: 5px;
1540+ padding-bottom: 5px;
1541+ font-weight: bold;
1542+}
1543+table.translation-stats tfoot th {
1544+ text-align:left;
1545+}
1546+table.translation-stats tfoot td {
1547+ text-align:center;
1548 }
1549
1550=== modified file 'lib/canonical/launchpad/icing/style.css'
1551--- lib/canonical/launchpad/icing/style.css 2009-08-31 00:06:29 +0000
1552+++ lib/canonical/launchpad/icing/style.css 2009-09-18 13:59:17 +0000
1553@@ -141,7 +141,7 @@
1554 .bullet {background:url(icon-sprites-2) 0 -1024px no-repeat;}
1555 .zoom-in {background:url(icon-sprites-2) 0 -1056px no-repeat;}
1556 .zoom-out {background:url(icon-sprites-2) 0 -1088px no-repeat;}
1557-.arquitecture {background:url(icon-sprites-2) 0 -1120px no-repeat;}
1558+.architecture {background:url(icon-sprites-2) 0 -1120px no-repeat;}
1559 .ppa-icon {background:url(icon-sprites-2) 0 -1147px no-repeat;}
1560 .ppa-icon-inactive {background:url(icon-sprites-2) 0 -1171px no-repeat;}
1561
1562@@ -308,7 +308,6 @@
1563 margin: 0 0 0.5em;
1564 /* Content headings are highlighted using a different color (the color is
1565 overriden per-application in subsequent rules) instead of bold text. */
1566- color: #83ad23;
1567 font-weight: normal;
1568 }
1569 h1 {font-size: 2em;}
1570@@ -675,30 +674,11 @@
1571
1572 /* === Universal page layout === */
1573
1574-/* All pages begin with a global header or a topline. */
1575-#globalheader {
1576- clear: both;
1577- position: relative;
1578- width: 100%;
1579- height: 21px;
1580- overflow: hidden;
1581- margin: 0 0 1em 0;
1582- padding: 0;
1583- background-color: #EEE;
1584- background-image: url(globalheader_bg.gif);
1585- background-repeat: repeat-x;
1586- background-position: top center;
1587-}
1588-#globalheader .sitemessage a {
1589- color: #FFF;
1590- text-decoration: underline;
1591-}
1592-/* Site-specific message. */
1593-#globalheader .sitemessage {
1594- text-align: center;
1595- color: #FFF;
1596- font-size: 13px;
1597- padding-top: 2px;
1598+/* All pages include the sitemessage in the footer if one is defined
1599+ in the config. */
1600+.sitemessage {
1601+ font-size: 13px;
1602+ text-align: right;
1603 }
1604
1605 /* === Login control === */
1606@@ -729,7 +709,8 @@
1607 margin-left: 0;
1608 list-style-type: none;
1609 clear: both;
1610- margin-bottom: 0.5em;
1611+ margin-bottom: 1em;
1612+ font-size: 80%;
1613 }
1614 ol.breadcrumbs li {
1615 display:inline;
1616@@ -1492,11 +1473,16 @@
1617 .boardCommentDetails table {
1618 margin: -0.5em -12px;
1619 }
1620+.boardCommentDetails tr {
1621+ width: 100%;
1622+}
1623 .boardCommentDetails td {
1624+ width:98%;
1625 padding: 0.5em 12px;
1626 }
1627 .boardCommentDetails td.bug-comment-index {
1628- border-right: 1px solid #ddd;
1629+ width:2%;
1630+ border-left: 1px solid #ddd;
1631 }
1632 .boardCommentBody {padding: 0.5em 12px 0;}
1633 .boardBugActivityBody {
1634@@ -1687,17 +1673,8 @@
1635 border: 1px solid gray;
1636 padding: 0.3em;
1637 margin: 1em 0 1em 0;
1638- float: left; /* So the border doesn't use 100% of the page */
1639-}
1640-
1641-div#build-status-summary {
1642- clear:right;
1643- float:right;
1644-}
1645-
1646-div#build-status-summary h2 {
1647- margin-top:0;
1648-}
1649+}
1650+
1651
1652 div#build-status-summary th {
1653 text-align:left;
1654@@ -1762,6 +1739,30 @@
1655 padding-left: 1em;
1656 }
1657
1658+/* PPA installation instructions slider.*/
1659+#pre-karmic-systems-slide-trigger {
1660+ cursor: pointer;
1661+ color: #093; /* for the underline on hover */
1662+}
1663+#pre-karmic-systems-slide-trigger:hover {
1664+ text-decoration: underline
1665+}
1666+#ppa-install-slide-trigger {
1667+ cursor: pointer;
1668+ color: #093; /* for the underline on hover */
1669+}
1670+#ppa-install-slide-trigger:hover {
1671+ text-decoration: underline
1672+}
1673+#ppa-install .widget-body {
1674+ margin: 1em;
1675+}
1676+#ppa-install .widget-body.lazr-closed {
1677+ height: 0px;
1678+ overflow: hidden;
1679+ display: block;
1680+}
1681+
1682 /* Brand the related PPA versions with the PPA icon (not currently
1683 in the sprite image.) */
1684 div#slide-trigger {
1685@@ -1831,6 +1832,13 @@
1686 max-width: 100%;
1687 }
1688
1689+/* <pre> for presenting source changelog nicely. */
1690+pre.changelog {
1691+ margin: 0.5em;
1692+ padding: 5px;
1693+ font: 116% monospace;
1694+}
1695+
1696 /* Disabled PPAs when linkified (uploaders) should be gray and when not
1697 linkified should be lined-through. */
1698 a.ppa-icon-inactive {
1699@@ -1844,6 +1852,18 @@
1700 padding: 1em 1em 2em 0em;
1701 }
1702
1703+/* XXX Michael Nelson 20090828 bug=420376
1704+ * Temporary style for actions on the ppa/+packages page until we move
1705+ * the copy/delete packages functionality into the page itself. */
1706+div.ppa-packaging-tmp-actions {
1707+ float:right;
1708+}
1709+
1710+div.ppa-packaging-tmp-actions .portlet {
1711+ border-top: 0 none;
1712+}
1713+
1714+
1715 /* --- Code --- */
1716
1717 body.tab-branches #applications {background: url(app-code-wm.gif) no-repeat;}
1718@@ -1920,6 +1940,7 @@
1719 #branch-pending-writes {
1720 background: #FFF59C;
1721 width: 40em;
1722+ margin: 1em auto;
1723 padding-left: 1em;
1724 padding-right: 1em;
1725 padding-top: 0.2em;
1726
1727=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
1728--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-24 12:06:17 +0000
1729+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-09-09 11:28:03 +0000
1730@@ -22,6 +22,8 @@
1731 patch_collection_return_type, patch_plain_parameter_type,
1732 patch_choice_parameter_type, patch_reference_property)
1733
1734+from canonical.launchpad.interfaces.structuralsubscription import (
1735+ IStructuralSubscription, IStructuralSubscriptionTarget)
1736 from lp.bugs.interfaces.bug import IBug
1737 from lp.bugs.interfaces.bugbranch import IBugBranch
1738 from lp.bugs.interfaces.bugnomination import IBugNomination
1739@@ -46,6 +48,7 @@
1740 from lp.registry.interfaces.distroseries import IDistroSeries
1741 from lp.registry.interfaces.person import IPerson, IPersonPublic
1742 from canonical.launchpad.interfaces.hwdb import HWBus, IHWSubmission
1743+from lp.registry.interfaces.pocket import PackagePublishingPocket
1744 from lp.registry.interfaces.product import IProduct
1745 from lp.registry.interfaces.productseries import IProductSeries
1746 from lp.soyuz.interfaces.archive import IArchive
1747@@ -59,7 +62,7 @@
1748 from lp.soyuz.interfaces.publishing import (
1749 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,
1750 ISecureSourcePackagePublishingHistory, ISourcePackagePublishingHistory,
1751- PackagePublishingPocket, PackagePublishingStatus)
1752+ PackagePublishingStatus)
1753 from lp.soyuz.interfaces.packageset import IPackageset
1754 from lp.soyuz.interfaces.queue import (
1755 IPackageUpload, PackageUploadCustomFormat, PackageUploadStatus)
1756@@ -277,3 +280,11 @@
1757 IPackageUpload['pocket'].vocabulary = PackagePublishingPocket
1758 patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)
1759 patch_reference_property(IPackageUpload, 'archive', IArchive)
1760+
1761+# IStructuralSubscription
1762+patch_reference_property(
1763+ IStructuralSubscription, 'target', IStructuralSubscriptionTarget)
1764+
1765+patch_reference_property(
1766+ IStructuralSubscriptionTarget, 'parent_subscription_target',
1767+ IStructuralSubscriptionTarget)
1768
1769=== modified file 'lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt'
1770--- lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2008-02-11 17:44:30 +0000
1771+++ lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2009-08-25 11:21:05 +0000
1772@@ -48,31 +48,100 @@
1773 >>> target.addBugSubscription(no_priv, no_priv)
1774 <StructuralSubscription ...>
1775
1776-Let's add an ITeam as one of the subscribers:
1777+People can only be subscribed by themselves, and only the team admins may
1778+subscribe a team.
1779+
1780+no-priv, who has no relationship to ubuntu-team, cannot subscribe it.
1781
1782 >>> ubuntu_team = personset.getByName("ubuntu-team")
1783- >>> target.addBugSubscription(ubuntu_team, ubuntu_team)
1784- <StructuralSubscription ...>
1785-
1786- >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1787- [u'no-priv', u'ubuntu-team']
1788+ >>> target.addBugSubscription(ubuntu_team, no_priv)
1789+ Traceback (most recent call last):
1790+ ...
1791+ UserCannotSubscribePerson: no-priv does not have permission to subscribe ubuntu-team.
1792+
1793+But kamion, an admin of the team, can.
1794+
1795+ >>> kamion = personset.getByName("kamion")
1796+ >>> target.addBugSubscription(ubuntu_team, kamion)
1797+ <StructuralSubscription ...>
1798+
1799+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1800+ [u'no-priv', u'ubuntu-team']
1801+
1802+foobar, a Launchpad administrator, can as well.
1803+
1804+ >>> foobar = personset.getByName("name16")
1805+ >>> target.addBugSubscription(ubuntu_team, foobar)
1806+ <StructuralSubscription ...>
1807+
1808+A non-admin cannot subscribe a person other than themselves.
1809+
1810+ >>> target.addBugSubscription(kamion, no_priv)
1811+ Traceback (most recent call last):
1812+ ...
1813+ UserCannotSubscribePerson: no-priv does not have permission to subscribe kamion.
1814+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1815+ [u'no-priv', u'ubuntu-team']
1816+
1817+But again, an admin can.
1818+
1819+ >>> target.addBugSubscription(kamion, foobar)
1820+ <StructuralSubscription ...>
1821+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1822+ [u'kamion', u'no-priv', u'ubuntu-team']
1823
1824 To remove a bug subscription, use
1825 IStructuralSubscriptionTarget.removeBugSubscription:
1826
1827- >>> target.removeBugSubscription(no_priv)
1828- >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1829- [u'ubuntu-team']
1830+ >>> target.removeBugSubscription(no_priv, no_priv)
1831+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1832+ [u'kamion', u'ubuntu-team']
1833+
1834+The subscription rules apply to unsubscription as well.
1835+
1836+An unprivileged user cannot unsubscribe a team.
1837+
1838+ >>> target.removeBugSubscription(ubuntu_team, no_priv)
1839+ Traceback (most recent call last):
1840+ ...
1841+ UserCannotSubscribePerson: no-priv does not have permission to unsubscribe ubuntu-team.
1842+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1843+ [u'kamion', u'ubuntu-team']
1844+
1845+But a team admin can.
1846+
1847+ >>> target.removeBugSubscription(ubuntu_team, kamion)
1848+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1849+ [u'kamion']
1850+
1851+An unprivileged user also cannot unsubscribe another user.
1852+
1853+ >>> target.removeBugSubscription(kamion, no_priv)
1854+ Traceback (most recent call last):
1855+ ...
1856+ UserCannotSubscribePerson: no-priv does not have permission to unsubscribe kamion.
1857+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1858+ [u'kamion']
1859+
1860+But the user themselves can.
1861+
1862+ >>> target.removeBugSubscription(kamion, kamion)
1863+ >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
1864+ []
1865
1866 Trying to remove a subscription that doesn't exist on a target raises a
1867 DeleteSubscriptionError.
1868
1869- >>> foobar = personset.getByName("name16")
1870- >>> target.removeBugSubscription(foobar)
1871+ >>> target.removeBugSubscription(foobar, foobar)
1872 Traceback (most recent call last):
1873 ...
1874 DeleteSubscriptionError: ...
1875
1876+Let's subscribe ubuntu-team again.
1877+
1878+ >>> target.addBugSubscription(ubuntu_team, foobar)
1879+ <StructuralSubscription ...>
1880+
1881 Trying to remove a bug subscription when notification levels for other
1882 applications are set, doesn't remove the subscription. Instead the
1883 notification level for bugs is set to NOTHING.
1884@@ -93,7 +162,7 @@
1885 >>> print_subscriptions_list(target.getSubscriptions())
1886 name16 COMMENTS LIFECYCLE
1887 ubuntu-team COMMENTS NOTHING
1888- >>> target.removeBugSubscription(foobar)
1889+ >>> target.removeBugSubscription(foobar, foobar)
1890 >>> print_subscriptions_list(target.getSubscriptions())
1891 name16 NOTHING LIFECYCLE
1892 ubuntu-team COMMENTS NOTHING
1893
1894=== modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py'
1895--- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-07-17 00:26:05 +0000
1896+++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-09-09 14:26:18 +0000
1897@@ -13,7 +13,8 @@
1898 'DeleteSubscriptionError',
1899 'IStructuralSubscription',
1900 'IStructuralSubscriptionForm',
1901- 'IStructuralSubscriptionTarget'
1902+ 'IStructuralSubscriptionTarget',
1903+ 'UserCannotSubscribePerson',
1904 ]
1905
1906 from zope.interface import Attribute, Interface
1907@@ -23,6 +24,14 @@
1908 from canonical.launchpad import _
1909 from canonical.launchpad.fields import (
1910 ParticipatingPersonChoice, PublicPersonChoice)
1911+from lp.registry.interfaces.person import IPerson
1912+
1913+from lazr.restful.declarations import (
1914+ REQUEST_USER, call_with, exported, export_as_webservice_entry,
1915+ export_factory_operation, export_read_operation, export_write_operation,
1916+ operation_parameters, operation_returns_collection_of,
1917+ operation_returns_entry, webservice_error)
1918+from lazr.restful.fields import Reference
1919
1920
1921 class BugNotificationLevel(DBEnumeratedType):
1922@@ -83,6 +92,7 @@
1923
1924 class IStructuralSubscription(Interface):
1925 """A subscription to a Launchpad structure."""
1926+ export_as_webservice_entry()
1927
1928 id = Int(title=_('ID'), readonly=True, required=True)
1929 product = Int(title=_('Product'), required=False, readonly=True)
1930@@ -95,13 +105,13 @@
1931 title=_('Distribution series'), required=False, readonly=True)
1932 sourcepackagename = Int(
1933 title=_('Source package name'), required=False, readonly=True)
1934- subscriber = ParticipatingPersonChoice(
1935+ subscriber = exported(ParticipatingPersonChoice(
1936 title=_('Subscriber'), required=True, vocabulary='ValidPersonOrTeam',
1937- readonly=True, description=_("The person subscribed."))
1938- subscribed_by = PublicPersonChoice(
1939+ readonly=True, description=_("The person subscribed.")))
1940+ subscribed_by = exported(PublicPersonChoice(
1941 title=_('Subscribed by'), required=True,
1942 vocabulary='ValidPersonOrTeam', readonly=True,
1943- description=_("The person creating the subscription."))
1944+ description=_("The person creating the subscription.")))
1945 bug_notification_level = Choice(
1946 title=_("Bug notification level"), required=True,
1947 vocabulary=BugNotificationLevel,
1948@@ -114,19 +124,30 @@
1949 default=BlueprintNotificationLevel.NOTHING,
1950 description=_("The volume and type of blueprint notifications "
1951 "this subscription will generate."))
1952- date_created = Datetime(
1953+ date_created = exported(Datetime(
1954 title=_("The date on which this subscription was created."),
1955- required=False)
1956- date_last_updated = Datetime(
1957+ required=False, readonly=True))
1958+ date_last_updated = exported(Datetime(
1959 title=_("The date on which this subscription was last updated."),
1960- required=False)
1961+ required=False, readonly=True))
1962
1963- target = Attribute("The structure to which this subscription belongs.")
1964+ target = exported(Reference(
1965+ schema=Interface, # IStructuralSubscriptionTarget
1966+ required=True, readonly=True,
1967+ title=_("The structure to which this subscription belongs.")))
1968
1969
1970 class IStructuralSubscriptionTarget(Interface):
1971 """A Launchpad Structure allowing users to subscribe to it."""
1972+ export_as_webservice_entry()
1973
1974+ # We don't really want to expose the level details yet. Only
1975+ # BugNotificationLevel.COMMENTS is used at this time.
1976+ @call_with(
1977+ min_bug_notification_level=BugNotificationLevel.COMMENTS,
1978+ min_blueprint_notification_level=BlueprintNotificationLevel.NOTHING)
1979+ @operation_returns_collection_of(IStructuralSubscription)
1980+ @export_read_operation()
1981 def getSubscriptions(min_bug_notification_level,
1982 min_blueprint_notification_level):
1983 """Return all the subscriptions with the specified levels.
1984@@ -148,11 +169,21 @@
1985 This method is used to create a new `IStructuralSubscription`
1986 for the target, with no levels set.
1987
1988- :subscriber: The IPerson who will be subscribed.
1989+ :subscriber: The IPerson who will be subscribed. If omitted,
1990+ subscribed_by will be used.
1991 :subscribed_by: The IPerson creating the subscription.
1992 :return: The new subscription.
1993 """
1994
1995+ @operation_parameters(
1996+ subscriber=Reference(
1997+ schema=IPerson,
1998+ title=_(
1999+ 'Person to subscribe. If omitted, the requesting user will be'
2000+ ' subscribed.'),
2001+ required=False))
2002+ @call_with(subscribed_by=REQUEST_USER)
2003+ @export_factory_operation(IStructuralSubscription, [])
2004 def addBugSubscription(subscriber, subscribed_by):
2005 """Add a bug subscription for this structure.
2006
2007@@ -160,21 +191,36 @@
2008 for the target with the bug notification level set to
2009 COMMENTS, the only level currently in use.
2010
2011- :subscriber: The IPerson who will be subscribed.
2012+ :subscriber: The IPerson who will be subscribed. If omitted,
2013+ subscribed_by will be used.
2014 :subscribed_by: The IPerson creating the subscription.
2015 :return: The new bug subscription.
2016 """
2017
2018- def removeBugSubscription(subscriber):
2019+ @operation_parameters(
2020+ subscriber=Reference(
2021+ schema=IPerson,
2022+ title=_(
2023+ 'Person to unsubscribe. If omitted, the requesting user will '
2024+ 'be unsubscribed.'),
2025+ required=False))
2026+ @call_with(unsubscribed_by=REQUEST_USER)
2027+ @export_write_operation()
2028+ def removeBugSubscription(subscriber, unsubscribed_by):
2029 """Remove a subscription to bugs from this structure.
2030
2031 If subscription levels for other applications are set,
2032 set the subscription's `bug_notification_level` to
2033 `NOTHING`, otherwise, destroy the subscription.
2034
2035- :subscriber: The IPerson who will be subscribed.
2036+ :subscriber: The IPerson who will be unsubscribed. If omitted,
2037+ unsubscribed_by will be used.
2038+ :unsubscribed_by: The IPerson removing the subscription.
2039 """
2040
2041+ @operation_parameters(person=Reference(schema=IPerson))
2042+ @operation_returns_entry(IStructuralSubscription)
2043+ @export_read_operation()
2044 def getSubscription(person):
2045 """Return the subscription for `person`, if it exists."""
2046
2047@@ -207,3 +253,9 @@
2048
2049 Raised when an error occurred trying to delete a
2050 structural subscription."""
2051+ webservice_error(400)
2052+
2053+
2054+class UserCannotSubscribePerson(Exception):
2055+ """User does not have permission to subscribe the person or team."""
2056+ webservice_error(401)
2057
2058=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
2059--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-26 20:50:26 +0000
2060+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-09-02 22:13:06 +0000
2061@@ -1211,6 +1211,7 @@
2062 var bugtarget_content = Y.get('#bugtarget-picker-' + conf.row_id);
2063 var status_content = tr.query('.status-content');
2064 var importance_content = tr.query('.importance-content');
2065+ var assignee_content = Y.get('#assignee-picker-' + conf.row_id);
2066 var milestone_content = tr.query('.milestone-content');
2067
2068 if (Y.Lang.isValue(LP.client.cache.bug) &&
2069@@ -1357,6 +1358,20 @@
2070 milestone_choice_edit);
2071 milestone_choice_edit.render();
2072 }
2073+ if (Y.Lang.isValue(assignee_content)) {
2074+ var assignee_picker = Y.lp.picker.addPickerPatcher(
2075+ 'ValidAssignee',
2076+ conf.bugtask_path,
2077+ "assignee_link",
2078+ assignee_content.get('id'),
2079+ true,
2080+ true,
2081+ {"step_title": "Search for people or teams",
2082+ "header": "Change assignee",
2083+ "remove_button_text": "Remove asignee",
2084+ "null_display_value": "Unassigned"});
2085+ assignee_picker.render()
2086+ }
2087 };
2088
2089 /**
2090
2091=== modified file 'lib/canonical/launchpad/javascript/registry/tests/milestone_table.html'
2092--- lib/canonical/launchpad/javascript/registry/tests/milestone_table.html 2009-06-19 18:31:25 +0000
2093+++ lib/canonical/launchpad/javascript/registry/tests/milestone_table.html 2009-09-14 15:03:45 +0000
2094@@ -22,7 +22,7 @@
2095 <table id="series-trunk" class="listing">
2096 <thead>
2097 <tr>
2098- <th>Version "Codename"</th>
2099+ <th>Version</th>
2100 </tr>
2101 </thead>
2102 <tbody id="milestone-rows">
2103
2104=== modified file 'lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js'
2105--- lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js 2009-06-30 21:06:27 +0000
2106+++ lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js 2009-08-28 16:06:56 +0000
2107@@ -19,9 +19,9 @@
2108 var lp_client = new LP.client.Launchpad();
2109
2110 /**
2111- * Configuration for the dynamic update of the build summary table
2112+ * Configuration for the dynamic update of the build summary portlet.
2113 */
2114- var build_summary_table_dynamic_update_config = {
2115+ var build_summary_portlet_dynamic_update_config = {
2116 uri: null, // Note: we have to defer setting the uri until later as
2117 // the LP.client.cache is not initialized until the end
2118 // of the page.
2119@@ -30,19 +30,19 @@
2120
2121 /**
2122 * This function knows how to update an Archive Build Status summary
2123- * table, when given an object of the form:
2124+ * when given an object of the form:
2125 * {total: 5, failed: 3}
2126 *
2127 * @config domUpdateFunction
2128 */
2129- domUpdateFunction: function(table_node, data_object){
2130- var td_nodelist = table_node.getElementsByTagName('td');
2131+ domUpdateFunction: function(portlet_node, data_object){
2132+ var counter_nodelist = portlet_node.queryAll('.build-count');
2133
2134- // For each node of the table's td elements
2135- td_nodelist.each(function(node){
2136+ // For each node of the counter node in the portlet:
2137+ counter_nodelist.each(function(node){
2138 // Check whether the node has a class matching the data name
2139 // of the passed in data, and if so, set the innerHTML to
2140- // thecorresponding value.
2141+ // the corresponding value.
2142 Y.each(data_object, function(data_value, data_name){
2143 if (node.hasClass(data_name)){
2144 previous_value = node.get("innerHTML");
2145@@ -67,9 +67,9 @@
2146 *
2147 * @config stopUpdatesCheckFunction
2148 */
2149- stopUpdatesCheckFunction: function(table_node){
2150+ stopUpdatesCheckFunction: function(portlet_node){
2151 // Stop updating only when there are zero pending builds:
2152- var pending_elem = table_node.query("td.pending");
2153+ var pending_elem = portlet_node.query(".pending");
2154 if (pending_elem === null){
2155 return true;
2156 }
2157@@ -83,13 +83,13 @@
2158 * Initialization of the build count summary dynamic table updates.
2159 */
2160 Y.on("domready", function(){
2161- // Grab the Archive build count table and tell it how to
2162+ // Grab the Archive build count portlet and tell it how to
2163 // update itself:
2164- var table = Y.get('table#build-count-table');
2165- build_summary_table_dynamic_update_config.uri =
2166+ var portlet = Y.get('div#build-status-summary');
2167+ build_summary_portlet_dynamic_update_config.uri =
2168 LP.client.cache.context.self_link;
2169- table.plug(Y.lp.DynamicDomUpdater,
2170- build_summary_table_dynamic_update_config);
2171+ portlet.plug(Y.lp.DynamicDomUpdater,
2172+ build_summary_portlet_dynamic_update_config);
2173 });
2174
2175 /**
2176
2177=== modified file 'lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt'
2178--- lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2009-08-30 13:31:02 +0000
2179+++ lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2009-09-17 15:24:12 +0000
2180@@ -53,7 +53,8 @@
2181 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
2182 &bull;...Launchpad...) devmode demo site
2183
2184- >>> print extract_text(find_tag_by_id(browser.contents, 'globalheader'))
2185+ >>> print extract_text(find_tags_by_class(
2186+ ... browser.contents, 'sitemessage')[0])
2187 This is a demo site mmk. File a bug.
2188 >>> print browser.getLink(url="http://example.com").text
2189 File a bug
2190@@ -68,7 +69,8 @@
2191 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
2192 |...Launchpad...) devmode demo site
2193
2194- >>> print extract_text(find_tag_by_id(browser.contents, 'globalheader'))
2195+ >>> print extract_text(find_tags_by_class(
2196+ ... browser.contents, 'sitemessage')[0])
2197 This is a demo site mmk. File a bug.
2198 >>> print browser.getLink(url="http://example.com").text
2199 File a bug
2200@@ -86,16 +88,91 @@
2201 >>> browser.open('http://launchpad.dev/ubuntu')
2202 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
2203 &bull;...Launchpad...) devmode
2204- >>> print find_tag_by_id(browser.contents, 'globalheader')
2205- None
2206+ >>> len(find_tags_by_class(browser.contents, 'sitemessage'))
2207+ 0
2208+
2209
2210 And for a non-3-0 page:
2211
2212 >>> browser.open('http://launchpad.dev/')
2213 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
2214 |...Launchpad...) devmode
2215- >>> print find_tag_by_id(browser.contents, 'globalheader')
2216- None
2217+ >>> len(find_tags_by_class(browser.contents, 'sitemessage'))
2218+ 0
2219+
2220+
2221+== Launchpad Edge ==
2222+
2223+Additionally, when a server is running as an edge server, the site-message
2224+is appended with a link to disable edge redirects.
2225+
2226+In addition to this prominent display on the root page, most pages will
2227+also include the disable-redirect link in the site_message - if the
2228+user is a member of the beta group and has not already disabled
2229+the redirects.
2230+
2231+ # Now setup an edge site-message config and re-check.
2232+ >>> edge_config_data = """
2233+ ... [launchpad]
2234+ ... site_message: This is a beta site.
2235+ ... is_edge: True
2236+ ... """
2237+ >>> config.push('edge_config_data', edge_config_data)
2238+ >>> beta_browser = setupBrowser(
2239+ ... auth='Basic beta-admin@launchpad.net:test')
2240+ >>> beta_browser.open('http://launchpad.dev/ubuntu')
2241+ >>> site_message = find_tags_by_class(
2242+ ... beta_browser.contents, 'sitemessage')[0]
2243+ >>> print extract_text(site_message)
2244+ This is a beta site. Disable edge redirect.
2245+ >>> print extract_text(site_message.find(
2246+ ... 'a', onclick="setBetaRedirect(false)"))
2247+ Disable edge redirect.
2248+
2249+The disable-redirect link will not appear for locationless pages, such
2250+as the login page, as the view does not inherit from LaunchpadView and
2251+so cannot support this functionality.
2252+
2253+ >>> beta_browser.open('http://launchpad.dev/+login')
2254+ >>> print extract_text(find_tags_by_class(
2255+ ... beta_browser.contents, 'sitemessage')[0])
2256+ This is a beta site.
2257+
2258+The disable-redirect link will not appear in the site_message when
2259+browsed by non-beta users.
2260+
2261+ >>> browser.open('http://launchpad.dev/ubuntu')
2262+ >>> print extract_text(find_tags_by_class(
2263+ ... browser.contents, 'sitemessage')[0])
2264+ This is a beta site.
2265+
2266+Similarly, once the redirection has been inhibited, the link changes to
2267+enable redirects..
2268+
2269+ # Workaround bug in mechanize where you cannot use the Cookie
2270+ # header with the CookieJar
2271+ >>> from mechanize._clientcookie import Cookie
2272+ >>> cookiejar = (
2273+ ... beta_browser.mech_browser._ua_handlers['_cookies'].cookiejar)
2274+ >>> cookiejar.set_cookie(
2275+ ... Cookie(
2276+ ... version=0, name='inhibit_beta_redirect', value='1', port=None,
2277+ ... port_specified=False, domain='.launchpad.dev',
2278+ ... domain_specified=True, domain_initial_dot=True, path='/',
2279+ ... path_specified=True, secure=False, expires=None,
2280+ ... discard=None, comment=None, comment_url=None, rest={}))
2281+ >>> beta_browser.open('http://launchpad.dev/ubuntu')
2282+ >>> site_message = find_tags_by_class(
2283+ ... beta_browser.contents, 'sitemessage')[0]
2284+ >>> print extract_text(site_message)
2285+ This is a beta site. Enable edge redirect.
2286+ >>> print extract_text(site_message.find(
2287+ ... 'a', onclick="setBetaRedirect(true)"))
2288+ Enable edge redirect.
2289+
2290+
2291+ # Remove the specific site-message config data before continuing.
2292+ >>> dummy = config.pop('edge_config_data')
2293
2294
2295 == Launchpad.net ==
2296
2297=== modified file 'lib/canonical/launchpad/pagetests/basics/marketing.txt'
2298--- lib/canonical/launchpad/pagetests/basics/marketing.txt 2009-06-12 16:36:02 +0000
2299+++ lib/canonical/launchpad/pagetests/basics/marketing.txt 2009-09-07 13:15:05 +0000
2300@@ -153,9 +153,9 @@
2301 === Bugs ===
2302
2303 >>> browser.open('http://bugs.launchpad.dev')
2304- >>> tour_link = browser.getLink('Take a tour')
2305+ >>> tour_link = browser.getLink('take a tour')
2306 >>> print tour_link.url
2307- http://launchpad.dev/+tour/bugs
2308+ http://bugs.launchpad.dev/+tour
2309 >>> tour_link.click()
2310
2311
2312
2313=== modified file 'lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt'
2314--- lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2009-08-27 07:05:16 +0000
2315+++ lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2009-09-16 22:57:42 +0000
2316@@ -67,8 +67,6 @@
2317 >>> check_redirect("/+index", host='feeds.launchpad.dev', status=301)
2318 >>> check("/+graphics")
2319
2320->>> check("/translations/+about")
2321-
2322 >>> check("/+imports", host='translations.launchpad.dev')
2323 >>> check("/+imports/1/+index", auth=True, host='translations.launchpad.dev')
2324 >>> check_not_found("/+imports/foo", host='translations.launchpad.dev')
2325@@ -202,10 +200,6 @@
2326 removing this, you must be completely sure that no supported Ubuntu release
2327 is still pointing to this old URL (see bug #138090).
2328
2329->>> check_redirect("/ubuntu/hoary/+source/evolution/+translate",
2330-... host="launchpad.dev", status=301)
2331->>> check("/ubuntu/hoary/+source/evolution/+translate",
2332-... host='translations.launchpad.dev' )
2333 >>> check_redirect("/ubuntu/hoary/+source/evolution/+translations", status=301)
2334 >>> check("/ubuntu/hoary/+source/evolution/+translations",
2335 ... host='translations.launchpad.dev')
2336@@ -408,7 +402,6 @@
2337 >>> check("/~name16/+edithomepage", auth=True)
2338 >>> check("/~name16/+review", auth=True)
2339 >>> check("/~name16/+portlet-emails")
2340->>> check("/~name16/+portlet-details")
2341 >>> check("/~name16/+portlet-team-assignedbugs")
2342 >>> check("/~name16/+specworkload")
2343 >>> check("/~name16/+imports", host='translations.launchpad.dev')
2344
2345=== modified file 'lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt'
2346--- lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2009-08-13 15:12:16 +0000
2347+++ lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2009-09-16 22:57:42 +0000
2348@@ -14,4 +14,4 @@
2349 >>> browser.open('http://launchpad.dev/~mark/')
2350 >>> print browser.contents
2351 <!DOCTYPE...
2352- ...<!-- at least ... queries issued in ... seconds -->...
2353+ ...<!--... At least ... queries issued in ... seconds ...-->...
2354
2355=== modified file 'lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt'
2356--- lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2009-07-23 00:09:16 +0000
2357+++ lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2009-09-05 06:40:36 +0000
2358@@ -12,17 +12,18 @@
2359
2360 The OOPS id is put into the comment at the end of the document.
2361
2362- >>> print browser.contents
2363- <!DOCTYPE ...
2364- ...
2365+ >>> (page, summary) = browser.contents.split('</html>')
2366+ >>> print summary
2367 <!-- at least ... queries issued in ... seconds OOPS-... -->
2368 <!-- Launchpad ... -->
2369
2370 The ++oops++ can be anywhere in the traversal.
2371
2372 >>> browser.open("http://launchpad.dev/gnome-terminal/++oops++/trunk")
2373- >>> print browser.contents
2374- <!DOCTYPE ...
2375- ...
2376- <!-- at least ... queries issued in ... seconds OOPS-... -->
2377- <!-- Launchpad ... -->
2378+ >>> (page, summary) = browser.contents.split('</html>')
2379+ >>> print summary
2380+ <!-- ...
2381+ At least ... queries issued in ... seconds OOPS-...
2382+ <BLANKLINE>
2383+ Launchpad ...
2384+ -->
2385
2386=== removed directory 'lib/canonical/launchpad/pagetests/distroseries'
2387=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
2388--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2009-07-06 14:31:45 +0000
2389+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2009-09-09 20:08:36 +0000
2390@@ -47,7 +47,6 @@
2391
2392 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
2393 >>> print extract_text(main_content)
2394- Authorize application to access Launchpad on your behalf
2395 The application identified as foobar123451432 wants to access Launchpad on
2396 your behalf. What level of access do you want to grant?
2397 ...
2398@@ -120,7 +119,6 @@
2399 ... % urlencode(params_with_context))
2400 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
2401 >>> print extract_text(main_content)
2402- Authorize application to access Launchpad on your behalf
2403 The application...wants to access things related to Mozilla Firefox...
2404
2405 A client other than a web browser may request a JSON representation of
2406
2407=== modified file 'lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt'
2408--- lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt 2009-07-01 13:16:44 +0000
2409+++ lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt 2009-09-15 15:42:39 +0000
2410@@ -19,7 +19,7 @@
2411 >>> my_browser = setupBrowser(auth='Basic salgado@ubuntu.com:zeca')
2412 >>> my_browser.open('http://launchpad.dev/~salgado/+oauth-tokens')
2413 >>> print my_browser.title
2414- Applications you authorized to access Launchpad
2415+ +oauth-tokens : Guilherme Salgado
2416 >>> main_content = find_tag_by_id(my_browser.contents, 'maincontent')
2417 >>> print extract_text(main_content)
2418 Authorized applications
2419@@ -41,7 +41,7 @@
2420 keys stored in hidden <input>s as well as the button to revoke the
2421 authorization.
2422
2423- >>> li = main_content.find('li')
2424+ >>> li = find_tag_by_id(main_content, 'tokens').find('li')
2425 >>> for input in li.find('form').findAll('input'):
2426 ... print input['name'], input['value']
2427 consumer_key foobar123451432
2428@@ -71,7 +71,7 @@
2429
2430 >>> my_browser.getControl('Revoke Authorization', index=1).click()
2431 >>> print my_browser.title
2432- Applications you authorized to access Launchpad
2433+ +oauth-tokens : Guilherme Salgado
2434 >>> for message in get_feedback_messages(my_browser.contents):
2435 ... print message
2436 Authorization revoked successfully.
2437
2438=== modified file 'lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt'
2439--- lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt 2009-08-11 21:34:18 +0000
2440+++ lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt 2009-09-09 23:16:08 +0000
2441@@ -124,8 +124,8 @@
2442
2443 >>> user_browser.open('http://launchpad.dev/bzr/trunk/')
2444 >>> user_browser.getLink('Link to Ubuntu package').click()
2445- >>> user_browser.title
2446- 'Ubuntu source packaging'
2447+ >>> print user_browser.title
2448+ +ubuntupkg : Series trunk : Bazaar
2449
2450 >>> user_browser.getControl(name='ubuntupkg').value = (
2451 ... "bzr<script>window.alert('XSS')</script>")
2452
2453=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt'
2454--- lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt 2006-11-29 13:24:27 +0000
2455+++ lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt 2009-09-15 10:01:13 +0000
2456@@ -9,9 +9,4 @@
2457 >>> 'Help and support' in anon_browser.contents
2458 True
2459
2460- >>> anon_browser.open(
2461- ... 'http://launchpad.dev/distros/ubuntu/hoary/+source/evolution/+translate'
2462- ... )
2463- >>> 'Help translate' in anon_browser.contents
2464- True
2465-
2466+
2467
2468=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt'
2469--- lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt 2009-05-12 01:39:29 +0000
2470+++ lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt 2009-09-09 23:16:08 +0000
2471@@ -55,11 +55,8 @@
2472
2473 >>> path = "%s/+validateemail" % base_path
2474 >>> browser.open(path)
2475- >>> print '\n' + browser.contents
2476- <BLANKLINE>
2477- ...
2478- ...Confirm e-mail address...martin.pitt@canonical.com...
2479- ...
2480+ >>> print browser.title
2481+ Confirm e-mail address
2482
2483 >>> browser.getControl('Continue').click()
2484 >>> browser.url
2485@@ -77,4 +74,3 @@
2486 >>> e = EmailAddressSet().getByEmail(to_addr)
2487 >>> e.status.title
2488 'Preferred Email Address'
2489-
2490
2491=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt'
2492--- lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2009-02-05 20:46:59 +0000
2493+++ lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2009-09-15 10:55:13 +0000
2494@@ -46,6 +46,7 @@
2495 500s: 0
2496 503s: 0
2497 5XXs: 0
2498+ 5XXs_b: 0
2499 6XXs: 0
2500 http requests: 0
2501 requests: 0
2502@@ -108,6 +109,26 @@
2503 http requests: 1
2504 requests: 1
2505
2506+We also have a special metric counting server errors returned to known
2507+web browsers (5XXs_b) - in the production environment we care more
2508+about errors returned to people than robots crawling obscure parts of
2509+the site.
2510+
2511+ >>> from textwrap import dedent
2512+ >>> output = http(dedent("""\
2513+ ... GET /error-test HTTP/1.1
2514+ ... Host: launchpad.dev
2515+ ... User-Agent: Mozilla/42.0
2516+ ... """))
2517+ >>> output.getStatus()
2518+ 500
2519+ >>> report()
2520+ 500s: 1
2521+ 5XXs: 1
2522+ 5XXs_b: 1
2523+ http requests: 1
2524+ requests: 1
2525+
2526 == Number of XML-RPC Faults ==
2527
2528 >>> try:
2529@@ -123,7 +144,6 @@
2530
2531 == Number of soft timeouts ==
2532
2533- >>> from textwrap import dedent
2534 >>> from canonical.config import config
2535 >>> test_data = dedent("""
2536 ... [database]
2537
2538=== added file 'lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt'
2539--- lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 1970-01-01 00:00:00 +0000
2540+++ lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 2009-08-27 04:16:41 +0000
2541@@ -0,0 +1,151 @@
2542+= Structural Subscriptions =
2543+
2544+Structural subscriptions can be obtained from any target: a project,
2545+project series, project group, distribution, distribution series or
2546+distribution source package.
2547+
2548+ >>> login('admin@canonical.com')
2549+ >>> eric_db = factory.makePerson(name='eric')
2550+ >>> michael_db = factory.makePerson(name='michael')
2551+ >>> pythons_db = factory.makeTeam(name='pythons', owner=michael_db)
2552+ >>> pythons_db.addMember(eric_db, michael_db)
2553+
2554+ >>> fooix_db = factory.makeProduct(name='fooix', owner=eric_db)
2555+ >>> fooix01_db = fooix_db.newSeries(eric_db, '0.1', 'Series 0.1')
2556+ >>> logout()
2557+
2558+We can list the structural subscriptions on a target using the
2559+getSubscriptions named operation. There are none just yet.
2560+
2561+ >>> from lazr.restful.testing.webservice import (
2562+ ... pprint_collection, pprint_entry)
2563+ >>> subscriptions = webservice.named_get(
2564+ ... '/fooix', 'getSubscriptions').jsonBody()
2565+ >>> pprint_collection(subscriptions)
2566+ start: None
2567+ total_size: 0
2568+ ---
2569+
2570+Now Eric subscribes to Fooix's bug notifications.
2571+
2572+ >>> from canonical.launchpad.testing.pages import webservice_for_person
2573+ >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
2574+ >>> eric_webservice = webservice_for_person(
2575+ ... eric_db, permission=OAuthPermission.WRITE_PRIVATE)
2576+
2577+ >>> print eric_webservice.named_post(
2578+ ... '/fooix', 'addBugSubscription')
2579+ HTTP/1.1 201 Created
2580+ ...
2581+ Location: http://.../fooix/+subscription/eric
2582+ ...
2583+
2584+ >>> subscriptions = webservice.named_get(
2585+ ... '/fooix', 'getSubscriptions').jsonBody()
2586+ >>> pprint_collection(subscriptions)
2587+ start: 0
2588+ total_size: 1
2589+ ---
2590+ date_created: u'...'
2591+ date_last_updated: u'...'
2592+ resource_type_link: u'http://.../#structural_subscription'
2593+ self_link: u'http://.../fooix/+subscription/eric'
2594+ subscribed_by_link: u'http://.../~eric'
2595+ subscriber_link: u'http://.../~eric'
2596+ target_link: u'http://.../fooix'
2597+ ---
2598+
2599+He can examine his subscription directly.
2600+
2601+ >>> pprint_entry(eric_webservice.named_get(
2602+ ... '/fooix', 'getSubscription',
2603+ ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
2604+ date_created: u'...'
2605+ date_last_updated: u'...'
2606+ resource_type_link: u'http://.../#structural_subscription'
2607+ self_link: u'http://.../fooix/+subscription/eric'
2608+ subscribed_by_link: u'http://.../~eric'
2609+ subscriber_link: u'http://.../~eric'
2610+ target_link: u'http://.../fooix'
2611+
2612+If the subscription doesn't exist, None will be returned.
2613+
2614+ >>> print webservice.named_get(
2615+ ... '/fooix', 'getSubscription',
2616+ ... person=webservice.getAbsoluteUrl('/~michael')).jsonBody()
2617+ None
2618+
2619+Eric can remove his subscription through the webservice.
2620+
2621+ >>> print eric_webservice.named_post(
2622+ ... '/fooix', 'removeBugSubscription')
2623+ HTTP/1.1 200 Ok...
2624+
2625+ >>> print webservice.named_get(
2626+ ... '/fooix', 'getSubscription',
2627+ ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()
2628+ None
2629+
2630+Teams can be subscribed by passing in the team as an argument. Eric
2631+tries this.
2632+
2633+ >>> print eric_webservice.named_post(
2634+ ... '/fooix', 'addBugSubscription',
2635+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
2636+ HTTP/1.1 401 Unauthorized
2637+ ...
2638+ UserCannotSubscribePerson: eric does not have permission to subscribe pythons.
2639+ <BLANKLINE>
2640+
2641+Oops, Eric isn't a team admin. Eric gets Michael to try, since he is an
2642+admin by virtue of his ownership.
2643+
2644+ >>> michael_webservice = webservice_for_person(
2645+ ... michael_db, permission=OAuthPermission.WRITE_PRIVATE)
2646+
2647+ >>> print michael_webservice.named_post(
2648+ ... '/fooix', 'addBugSubscription',
2649+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
2650+ HTTP/1.1 201 Created
2651+ ...
2652+ Location: http://.../fooix/+subscription/pythons
2653+ ...
2654+
2655+ >>> subscriptions = webservice.named_get(
2656+ ... '/fooix', 'getSubscriptions').jsonBody()
2657+ >>> pprint_collection(subscriptions)
2658+ start: 0
2659+ total_size: 1
2660+ ---
2661+ date_created: u'...'
2662+ date_last_updated: u'...'
2663+ resource_type_link: u'http://.../#structural_subscription'
2664+ self_link: u'http://.../fooix/+subscription/pythons'
2665+ subscribed_by_link: u'http://.../~michael'
2666+ subscriber_link: u'http://.../~pythons'
2667+ target_link: u'http://.../fooix'
2668+ ---
2669+
2670+Eric can't unsubscribe the team either.
2671+
2672+ >>> print eric_webservice.named_post(
2673+ ... '/fooix', 'removeBugSubscription',
2674+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
2675+ HTTP/1.1 401 Unauthorized
2676+ ...
2677+ UserCannotSubscribePerson: eric does not have permission to unsubscribe pythons.
2678+ <BLANKLINE>
2679+
2680+Michael can, though.
2681+
2682+ >>> print michael_webservice.named_post(
2683+ ... '/fooix', 'removeBugSubscription',
2684+ ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
2685+ HTTP/1.1 200 Ok...
2686+
2687+ >>> subscriptions = webservice.named_get(
2688+ ... '/fooix', 'getSubscriptions').jsonBody()
2689+ >>> pprint_collection(subscriptions)
2690+ start: None
2691+ total_size: 0
2692+ ---
2693
2694=== modified file 'lib/canonical/launchpad/pagetitles.py'
2695--- lib/canonical/launchpad/pagetitles.py 2009-09-01 15:42:27 +0000
2696+++ lib/canonical/launchpad/pagetitles.py 2009-09-18 09:21:50 +0000
2697@@ -76,17 +76,6 @@
2698 return self.text % context.displayname
2699
2700
2701-class FilteredTranslationsTitle(SubstitutionHelper):
2702- """Return the formatted string with context's title and view's person."""
2703- def __call__(self, context, view):
2704- if view.person is not None:
2705- person = view.person.displayname
2706- else:
2707- person = 'unknown'
2708- return self.text % {'title' : context.title,
2709- 'person' : person }
2710-
2711-
2712 class ContextId(SubstitutionHelper):
2713 """Return the formatted string with context's id."""
2714 def __call__(self, context, view):
2715@@ -139,8 +128,6 @@
2716
2717 archive_edit_dependencies = ContextDisplayName('Edit dependencies for %s')
2718
2719-archive_index = ContextDisplayName('%s')
2720-
2721 archive_subscriber_edit = ContextDisplayName('Edit %s')
2722
2723 archive_subscribers = ContextDisplayName('Manage access to %s')
2724@@ -178,8 +165,6 @@
2725
2726 bug_branch_add = LaunchbagBugID('Bug #%d - Add branch')
2727
2728-bug_cve = LaunchbagBugID("Bug #%d - Add CVE reference")
2729-
2730 bug_edit = ContextBugId('Bug #%d - Edit')
2731
2732 bug_edit_confirm = ContextBugId('Bug #%d - Edit confirmation')
2733@@ -198,23 +183,14 @@
2734
2735 bug_nominate_for_series = ViewLabel()
2736
2737-bug_removecve = LaunchbagBugID("Bug #%d - Remove CVE reference")
2738-
2739 bug_secrecy = ContextBugId('Bug #%d - Set visibility')
2740
2741 bug_subscription = LaunchbagBugID('Bug #%d - Subscription options')
2742
2743-bug_remove_question = LaunchbagBugID(
2744- 'Bug #%d - Convert this question back to a bug')
2745-
2746 bugbranch_delete = 'Delete bug branch link'
2747
2748 bugbranch_edit = "Edit branch fix status"
2749
2750-def bugcomment_index(context, view):
2751- """Return the page title for a bug comment."""
2752- return "Bug #%d - Comment #%d" % (context.bug.id, view.comment.index)
2753-
2754 buglinktarget_linkbug = 'Link to bug report'
2755
2756 buglinktarget_unlinkbugs = 'Remove links to bug reports'
2757@@ -227,23 +203,11 @@
2758 """Return the view's page heading."""
2759 return view.getSearchPageHeading()
2760
2761-bug_listing_expirable = ContextTitle("Bugs that can expire in %s")
2762-
2763 def bugnomination_edit(context, view):
2764 """Return the title for the page to manage bug nominations."""
2765 return 'Manage nomination for bug #%d in %s' % (
2766 context.bug.id, context.target.bugtargetdisplayname)
2767
2768-def bugwatch_editform(context, view):
2769- """Return the title for the page to edit an external bug watch."""
2770- return 'Bug #%d - Edit external bug watch (%s in %s)' % (
2771- context.bug.id, context.remotebug, context.bugtracker.title)
2772-
2773-def bugwatch_comments(context, view):
2774- """Return the title for a page of imported comments for a bug watch."""
2775- return "Bug #%d - Comments imported from bug watch %s on %s" % (
2776- context.bug.id, context.remotebug, context.bugtracker.title)
2777-
2778 def bugs_assigned(context, view):
2779 """Return the page title for the bugs assigned to the logged-in user."""
2780 if view.user:
2781@@ -293,27 +257,6 @@
2782 # bugtask_macros_buglisting contains only macros
2783 # bugtasks_index is a redirect
2784
2785-bugtracker_edit = ContextTitle(
2786- smartquote('Change details for "%s" bug tracker'))
2787-
2788-bugtracker_index = ContextTitle(smartquote('Bug tracker "%s"'))
2789-
2790-bugtrackers_add = 'Register an external bug tracker'
2791-
2792-bugtrackers_index = 'Bug trackers registered in Launchpad'
2793-
2794-build_buildlog = ContextTitle('Build log for %s')
2795-
2796-build_changes = ContextTitle('Changes in %s')
2797-
2798-build_index = ContextTitle('%s')
2799-
2800-build_retry = ContextTitle('Retry %s')
2801-
2802-build_rescore = ContextTitle('Rescore %s')
2803-
2804-builders_index = 'Launchpad build farm'
2805-
2806 calendar_index = ContextTitle('%s')
2807
2808 calendar_event_addform = ContextTitle('Add event to %s')
2809@@ -357,8 +300,6 @@
2810
2811 codeofconduct_admin = 'Administer Codes of Conduct'
2812
2813-codeofconduct_index = ContextTitle('%s')
2814-
2815 codeofconduct_list = 'Ubuntu Codes of Conduct'
2816
2817 def contact_user(context, view):
2818@@ -380,8 +321,6 @@
2819
2820 distributionmirror_index = ContextTitle('Mirror %s')
2821
2822-distribution_allpackages = ContextTitle('All packages in %s')
2823-
2824 distribution_archive_list = ContextTitle('%s Copy Archives')
2825
2826 distribution_upstream_bug_report = ContextTitle('Upstream Bug Report for %s')
2827@@ -392,8 +331,6 @@
2828
2829 distribution_mirrors = ContextTitle("Mirrors of %s")
2830
2831-distribution_series = ContextTitle("%s version history")
2832-
2833 distribution_translations = ContextDisplayName('Translating %s')
2834
2835 distribution_translation_settings = ContextTitle(
2836@@ -405,8 +342,6 @@
2837
2838 distribution_builds = ContextTitle('%s builds')
2839
2840-distribution_ppa_list = ContextTitle('%s Personal Package Archives')
2841-
2842 distributionsourcepackage_bugs = ContextTitle('Bugs in %s')
2843
2844 distributionsourcepackage_index = ContextTitle('%s')
2845@@ -414,11 +349,6 @@
2846 distributionsourcepackage_publishinghistory = ContextTitle(
2847 'Publishing history of %s')
2848
2849-structural_subscriptions_manage = ContextTitle(
2850- 'Bug subscriptions for %s')
2851-
2852-distributionsourcepackagerelease_index = ContextTitle('%s')
2853-
2854 distroarchseries_index = ContextTitle('%s in Launchpad')
2855
2856 distroarchseries_builds = ContextTitle('%s builds')
2857@@ -434,33 +364,15 @@
2858
2859 distroseries_cvereport = ContextDisplayName('CVE report for %s')
2860
2861-def distroseries_index(context, view):
2862- """Return the distribution and version page title."""
2863- return '%s %s in Launchpad' % (
2864- context.distribution.title, context.version)
2865-
2866 def distroseries_language_packs(context, view):
2867 return view.page_title
2868
2869-distroseries_packaging = ContextDisplayName('Mapping packages to upstream '
2870- 'for %s')
2871-
2872-distroseries_search = ContextDisplayName('Search packages in %s')
2873-
2874 distroseries_translations = ContextTitle('Translations of %s in Launchpad')
2875
2876-distroseries_builds = ContextTitle('%s builds')
2877-
2878 distroseries_queue = ContextTitle('Queue for %s')
2879
2880-distroseriesbinarypackage_index = ContextTitle('%s')
2881-
2882-distroserieslanguage_index = ContextTitle('%s')
2883-
2884 distroseriessourcepackagerelease_index = ContextTitle('%s')
2885
2886-edit_bug_supervisor = ContextTitle('Edit bug supervisor for %s')
2887-
2888 errorservice_config = 'Configure error log'
2889
2890 errorservice_entry = 'Error log entry'
2891@@ -503,18 +415,12 @@
2892
2893 hassprints_sprints = ContextTitle("Events related to %s")
2894
2895-hastranslationimports_index = 'Translation import queue'
2896-
2897 hwdb_fingerprint_submissions = (
2898 "Hardware Database submissions for a fingerprint")
2899
2900 hwdb_submit_hardware_data = (
2901 'Submit New Data to the Launchpad Hardware Database')
2902
2903-language_index = ContextDisplayName("%s in Launchpad")
2904-
2905-languageset_index = 'Languages in Launchpad'
2906-
2907 # launchpad_debug doesn't need a title.
2908
2909 def launchpad_addform(context, view):
2910@@ -538,30 +444,20 @@
2911
2912 # launchpad_js is standard javascript
2913
2914-launchpad_invalidbatchsize = "Invalid Batch Size"
2915-
2916 launchpad_legal = 'Launchpad legalese'
2917
2918 launchpad_login = 'Log in or register with Launchpad'
2919
2920-launchpad_notfound = 'Error: Page not found'
2921-
2922 launchpad_onezerostatus = 'One-Zero Page Template Status'
2923
2924-launchpad_requestexpired = 'Error: Timeout'
2925-
2926 def launchpad_search(context, view):
2927 """Return the page title corresponding to the user's search."""
2928 return view.page_title
2929
2930-launchpad_translationunavailable = 'Translation page is not available'
2931-
2932 launchpad_unexpectedformdata = 'Error: Unexpected form data'
2933
2934 launchpad_librarianfailure = "Sorry, you can't do this right now"
2935
2936-launchpad_readonlyfailure = "Sorry, you can't do this right now"
2937-
2938 # launchpad_widget_macros doesn't need a title.
2939
2940 launchpadstatisticset_index = 'Launchpad statistics'
2941@@ -579,31 +475,8 @@
2942
2943 loginservice_login = 'Launchpad Login Service'
2944
2945-logintoken_claimprofile = 'Claim Launchpad profile'
2946-
2947-logintoken_claimteam = 'Claim Launchpad team'
2948-
2949-# This page will always redirect the user to another page specific to the
2950-# login token in question, except when the token has been consumed already, in
2951-# which case the user will see the title.
2952-logintoken_index = 'You have already done this'
2953-
2954-logintoken_mergepeople = 'Merge Launchpad accounts'
2955-
2956-logintoken_newaccount = 'Create a new Launchpad account'
2957-
2958-logintoken_resetpassword = 'Forgotten your password?'
2959-
2960 loginservice_standalone_login = loginservice_login
2961
2962-logintoken_validateemail = 'Confirm e-mail address'
2963-
2964-logintoken_validategpg = 'Confirm OpenPGP key'
2965-
2966-logintoken_validatesignonlygpg = 'Confirm sign-only OpenPGP key'
2967-
2968-logintoken_validateteamemail = 'Confirm e-mail address'
2969-
2970 # main_template has the code to insert one of these titles.
2971
2972 malone_about = 'About Launchpad Bugs'
2973@@ -644,8 +517,6 @@
2974 """Return the view's pagetitle."""
2975 return view.pagetitle
2976
2977-marketing_translations_about = "About Translations"
2978-
2979 marketing_translations_faq = "FAQs about Translations"
2980
2981 mentoringofferset_success = "Successful mentorships over the past year."
2982@@ -658,8 +529,6 @@
2983
2984 milestone_add = ContextTitle('Add new milestone for %s')
2985
2986-milestone_index = ContextTitle('%s')
2987-
2988 milestone_edit = ContextTitle('Edit %s')
2989
2990 milestone_delete = ContextTitle('Delete %s')
2991@@ -689,8 +558,6 @@
2992 """Return the page title to change the driver."""
2993 return view.page_title
2994
2995-object_milestones = ContextTitle(smartquote("%s's milestones"))
2996-
2997 # object_pots is a fragment.
2998
2999 object_translations = ContextDisplayName('Translation templates for %s')
3000@@ -723,8 +590,6 @@
3001
3002 openidrpconfigset_index = 'OpenID Relying Party Configurations'
3003
3004-official_bug_target_manage_tags = 'Manage Official Bug Tags'
3005-
3006 def package_bugs(context, view):
3007 """Return the page title bug in a package."""
3008 return 'Bugs in %s' % context.name
3009@@ -741,8 +606,6 @@
3010
3011 people_requestmerge_multiple = 'Merge Launchpad accounts'
3012
3013-active_reviews = ContextDisplayName('Pending proposals for %s')
3014-
3015 person_archive_subscription = ContextDisplayName('%s')
3016
3017 person_archive_subscriptions = 'Private PPA access'
3018@@ -750,33 +613,6 @@
3019 person_answer_contact_for = ContextDisplayName(
3020 'Projects for which %s is an answer contact')
3021
3022-person_changepassword = 'Change your password'
3023-
3024-person_claim = 'Claim account'
3025-
3026-person_claim_team = 'Claim team'
3027-
3028-person_deactivate_account = 'Deactivate your Launchpad account'
3029-
3030-person_codesofconduct = ContextDisplayName(
3031- smartquote("%s's code of conduct signatures"))
3032-
3033-person_edit = ContextDisplayName(smartquote("%s's details"))
3034-
3035-person_editemails = ContextDisplayName(smartquote("%s's e-mail addresses"))
3036-
3037-person_editlocation = ContextDisplayName(smartquote("%s's usual location"))
3038-
3039-person_editpgpkeys = ContextDisplayName(smartquote("%s's OpenPGP keys"))
3040-
3041-person_editircnicknames = ContextDisplayName(smartquote("%s's IRC nicknames"))
3042-
3043-person_editjabberids = ContextDisplayName(smartquote("%s's Jabber IDs"))
3044-
3045-person_editsshkeys = ContextDisplayName(smartquote("%s's SSH keys"))
3046-
3047-person_editwikinames = ContextDisplayName(smartquote("%s's wiki names"))
3048-
3049 # person_foaf is an rdf file
3050
3051 person_hwdb_submissions = ContextDisplayName(
3052@@ -784,69 +620,27 @@
3053
3054 person_images = ContextDisplayName(smartquote("%s's hackergotchi and emblem"))
3055
3056-def person_index(context, view):
3057- """Return the page title to the person index page."""
3058- if context.is_valid_person_or_team:
3059- return '%s in Launchpad' % context.displayname
3060- else:
3061- return "%s does not use Launchpad" % context.displayname
3062-
3063 person_karma = ContextDisplayName(smartquote("%s's karma in Launchpad"))
3064
3065-person_maintained_packages = ContextDisplayName('Software maintained by %s')
3066-
3067 person_mentoringoffers = ContextTitle('Mentoring offered by %s')
3068
3069 def person_mergeproposals(context, view):
3070 """Return the view's heading."""
3071 return view.heading
3072
3073-person_oauth_tokens = "Applications you authorized to access Launchpad"
3074-
3075 person_packagebugs = ContextDisplayName("%s's package bug reports")
3076
3077 person_packagebugs_overview = person_packagebugs
3078
3079 person_packagebugs_search = person_packagebugs
3080
3081-person_participation = ContextTitle("Team participation by %s")
3082-
3083-person_ppa_packages = ContextDisplayName('PPA packages related to %s')
3084-
3085-person_related_projects = ContextDisplayName('Projects related to %s')
3086-
3087-person_related_software = ContextDisplayName('Software related to %s')
3088-
3089-person_review = ContextDisplayName("Review %s")
3090-
3091 person_specfeedback = ContextDisplayName('Feature feedback requests for %s')
3092
3093 person_specworkload = ContextDisplayName('Blueprint workload for %s')
3094
3095-person_translations = ContextDisplayName('Translations related to %s')
3096-
3097-person_translations_relicensing = "Translations licensing"
3098-
3099 person_translations_to_review = ContextDisplayName(
3100 'Translations for review by %s')
3101
3102-person_teamhierarchy = ContextDisplayName('Team hierarchy for %s')
3103-
3104-person_uploaded_packages = ContextDisplayName('Software uploaded by %s')
3105-
3106-person_vouchers = ContextDisplayName(
3107- 'Commercial subscription vouchers for %s')
3108-
3109-pofile_filter = FilteredTranslationsTitle(
3110- smartquote('Translations by %(person)s in "%(title)s"'))
3111-
3112-pofile_index = ContextTitle(smartquote('Translation overview for "%s"'))
3113-
3114-def pofile_translate(context, view):
3115- """Return the page to translate a template into a language."""
3116- return 'Translating %s into %s' % (
3117- context.potemplate.displayname, context.language.englishname)
3118-
3119 # portlet_* are portlets
3120
3121 poll_edit = ContextTitle(smartquote('Edit poll "%s"'))
3122@@ -869,17 +663,12 @@
3123
3124 poll_vote_simple = ContextTitle(smartquote('Vote in poll "%s"'))
3125
3126-potemplate_index = ContextTitle(smartquote('Translation status for "%s"'))
3127-
3128 product_admin = ContextTitle('Administer %s in Launchpad')
3129
3130 product_bugs = ContextDisplayName('Bugs in %s')
3131
3132 product_code_index = ContextDisplayName("Bazaar branches of %s")
3133
3134-product_distros = ContextDisplayName(
3135- '%s packages: Comparison of distributions')
3136-
3137 product_cvereport = ContextTitle('CVE reports for %s')
3138
3139 product_edit = 'Change project details'
3140@@ -893,57 +682,25 @@
3141 """Return the view's heading."""
3142 return view.heading
3143
3144-def product_new(context, view):
3145- """Return the view's heading."""
3146- return view.heading
3147-
3148 product_new_guided = 'Before you register your project...'
3149
3150-product_packages = ContextDisplayName('%s packages in Launchpad')
3151-
3152 product_purchase_subscription = ContextDisplayName(
3153 'Purchase Subscription for %s')
3154
3155 product_review_license = ContextTitle('Review %s')
3156
3157-product_series = ContextDisplayName('%s timeline')
3158-
3159 product_timeline = ContextTitle('Timeline Diagram for %s')
3160
3161 product_translations = ContextTitle('Translations of %s in Launchpad')
3162
3163-productrelease_add = ContextDisplayName('Publish the release of %s')
3164-
3165-productrelease_add_from_series = productrelease_add
3166-
3167-productrelease_delete = ContextTitle('Delete %s in Launchpad')
3168-
3169-productrelease_file_add = ContextDisplayName('Add a file to %s')
3170-
3171 productrelease_admin = ContextTitle('Administer %s in Launchpad')
3172
3173-productrelease_edit = ContextDisplayName('Edit details of %s in Launchpad')
3174-
3175 productrelease_index = ContextDisplayName('%s in Launchpad')
3176
3177-products_review_licenses = 'Review projects'
3178-
3179-productserieslanguage_index = ContextTitle('%s')
3180-
3181-productseries_index = ContextTitle('%s')
3182-
3183-productseries_packaging = ContextDisplayName(
3184- 'Packaging of %s in distributions')
3185-
3186 productseries_translations = ContextTitle('Translations overview for %s')
3187
3188 productseries_translations_settings = 'Settings for translations'
3189
3190-productseries_translations_bzr_import = (
3191- 'Request translations import from Bazaar branch')
3192-
3193-project_add = 'Register a project group with Launchpad'
3194-
3195 project_index = ContextTitle('%s in Launchpad')
3196
3197 project_bugs = ContextTitle('Bugs in %s')
3198@@ -1049,22 +806,10 @@
3199
3200 sourcepackage_builds = ContextTitle('Builds for %s')
3201
3202-sourcepackage_translate = ContextTitle('Help translate %s')
3203-
3204 sourcepackage_changelog = 'Source package changelog'
3205
3206 sourcepackage_filebug = ContextTitle("Report a bug about %s")
3207
3208-sourcepackage_gethelp = ContextTitle('Help and support options for %s')
3209-
3210-sourcepackage_packaging = ContextTitle('%s upstream links')
3211-
3212-def sourcepackage_index(context, view):
3213- """Return the page title for a source package in a distroseries."""
3214- return '%s source packages' % context.distroseries.title
3215-
3216-sourcepackage_translate = ContextTitle('Help translate %s')
3217-
3218 sourcepackagenames_index = 'Source package name set'
3219
3220 sourcepackagerelease_index = ContextTitle('Source package %s')
3221@@ -1135,8 +880,6 @@
3222 """Return the page title for subscribing to a specification."""
3223 return "Subscription of %s" % context.person.displayname
3224
3225-specificationtarget_documentation = ContextTitle('Documentation for %s')
3226-
3227 specificationtarget_index = ContextTitle('Blueprint listing for %s')
3228
3229 def specificationtarget_specs(context, view):
3230@@ -1173,48 +916,8 @@
3231
3232 standardshipitrequest_edit = 'Edit standard option'
3233
3234-team_addmember = ContextBrowsername('Add members to %s')
3235-
3236-team_add_my_teams = 'Propose/add one of your teams to another one'
3237-
3238-team_editproposed = ContextBrowsername('Proposed members of %s')
3239-
3240 team_index = ContextBrowsername('%s in Launchpad')
3241
3242-team_invitations = ContextBrowsername("Invitations sent to %s")
3243-
3244-team_join = ContextBrowsername('Join %s')
3245-
3246-team_leave = ContextBrowsername('Leave %s')
3247-
3248-team_mailinglist = 'Configure mailing list'
3249-
3250-team_mailinglist_moderate = 'Moderate mailing list'
3251-
3252-team_mailinglist_subscribers = ContextBrowsername(
3253- 'Mailing list subscribers for the %s team')
3254-
3255-team_map = ContextBrowsername('Map of %s participants')
3256-
3257-team_members = ContextBrowsername(smartquote('"%s" members'))
3258-
3259-team_mugshots = ContextBrowsername(smartquote('Mugshots in the "%s" team'))
3260-
3261-def teammembership_index(context, view):
3262- """Return the page title to the persons status in a team."""
3263- return smartquote("%s's membership status in %s") % (
3264- context.person.displayname, context.team.displayname)
3265-
3266-def teammembership_invitation(context, view):
3267- """Return the page title to invite a person to become a team member."""
3268- return "Make %s a member of %s" % (
3269- context.person.displayname, context.team.displayname)
3270-
3271-def teammembership_self_renewal(context, view):
3272- """Return the page title renew membership in a team."""
3273- return "Renew membership of %s in %s" % (
3274- context.person.displayname, context.team.displayname)
3275-
3276 team_mentoringoffers = ContextTitle('Mentoring available for newcomers to %s')
3277
3278 team_newpoll = ContextTitle('New poll for team %s')
3279@@ -1235,30 +938,6 @@
3280
3281 token_authorized = 'Almost finished ...'
3282
3283-translationgroup_index = ContextTitle(
3284- smartquote('"%s" Launchpad translation group'))
3285-
3286-translationgroup_appoint = ContextTitle(
3287- smartquote('Appoint a new translator to "%s"'))
3288-
3289-translationgroup_edit = ContextTitle(smartquote(
3290- 'Edit "%s" translation group details'))
3291-
3292-translationgroup_reassignment = ContextTitle(smartquote(
3293- 'Change the owner of "%s" translation group'))
3294-
3295-translationgroups_index = 'Launchpad translation groups'
3296-
3297 translationimportqueueentry_index = 'Translation import queue entry'
3298
3299-translationimportqueue_index = 'Translation import queue'
3300-
3301-translationimportqueue_blocked = 'Translation import queue - Blocked'
3302-
3303-def translationmessage_translate(context, view):
3304- """Return the page to translate a template into a language per message."""
3305- return 'Translating %s into %s' % (
3306- context.pofile.potemplate.displayname,
3307- context.pofile.language.englishname)
3308-
3309 unauthorized = 'Error: Not authorized'
3310
3311=== modified file 'lib/canonical/launchpad/scripts/hardware-1_0.rng'
3312--- lib/canonical/launchpad/scripts/hardware-1_0.rng 2009-02-24 17:43:37 +0000
3313+++ lib/canonical/launchpad/scripts/hardware-1_0.rng 2009-09-14 09:16:45 +0000
3314@@ -98,40 +98,64 @@
3315 </element>
3316 </zeroOrMore>
3317 </element>
3318+ <optional>
3319+ <element name="kernel-release">
3320+ <attribute name="value">
3321+ <text/>
3322+ </attribute>
3323+ </element>
3324+ </optional>
3325 </interleave>
3326 </define>
3327
3328 <define name="hardwareSection">
3329 <interleave>
3330- <element name="hal">
3331- <attribute name="version">
3332- <text/>
3333- </attribute>
3334- <oneOrMore>
3335- <element name="device">
3336- <attribute name="id">
3337- <data type="integer">
3338- <except>
3339- <value/>
3340- </except>
3341- </data>
3342- </attribute>
3343- <attribute name="udi">
3344- <text/>
3345- </attribute>
3346- <optional>
3347- <attribute name="parent">
3348- <data type="integer"/>
3349- </attribute>
3350- </optional>
3351- <!-- XXX: Abel Deuring 2007-12-07:
3352- specify a set of required properties? -->
3353- <oneOrMore>
3354- <ref name="property"/>
3355- </oneOrMore>
3356- </element>
3357- </oneOrMore>
3358- </element>
3359+ <choice>
3360+ <element name="hal">
3361+ <attribute name="version">
3362+ <text/>
3363+ </attribute>
3364+ <oneOrMore>
3365+ <element name="device">
3366+ <attribute name="id">
3367+ <data type="integer">
3368+ <except>
3369+ <value/>
3370+ </except>
3371+ </data>
3372+ </attribute>
3373+ <attribute name="udi">
3374+ <text/>
3375+ </attribute>
3376+ <optional>
3377+ <attribute name="parent">
3378+ <data type="integer"/>
3379+ </attribute>
3380+ </optional>
3381+ <!-- XXX: Abel Deuring 2007-12-07:
3382+ specify a set of required properties? -->
3383+ <oneOrMore>
3384+ <ref name="property"/>
3385+ </oneOrMore>
3386+ </element>
3387+ </oneOrMore>
3388+ </element>
3389+ <group>
3390+ <interleave>
3391+ <element name="udev">
3392+ <text/>
3393+ </element>
3394+ <element name="dmi">
3395+ <text/>
3396+ </element>
3397+ <element name="sysfs-attributes">
3398+ <zeroOrMore>
3399+ <text/>
3400+ </zeroOrMore>
3401+ </element>
3402+ </interleave>
3403+ </group>
3404+ </choice>
3405 <element name="processors">
3406 <oneOrMore>
3407 <element name="processor">
3408@@ -156,11 +180,7 @@
3409 <zeroOrMore>
3410 <element name="alias">
3411 <attribute name="target">
3412- <data type="integer">
3413- <except>
3414- <value/>
3415- </except>
3416- </data>
3417+ <text/>
3418 </attribute>
3419 <interleave>
3420 <element name="vendor">
3421@@ -292,11 +312,7 @@
3422 <zeroOrMore>
3423 <element name="target">
3424 <attribute name="id">
3425- <data type="integer">
3426- <except>
3427- <value/>
3428- </except>
3429- </data>
3430+ <text/>
3431 </attribute>
3432 <interleave>
3433 <zeroOrMore>
3434
3435=== modified file 'lib/canonical/launchpad/scripts/hwdbsubmissions.py'
3436--- lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-06-25 05:30:52 +0000
3437+++ lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-09-15 17:05:32 +0000
3438@@ -16,7 +16,10 @@
3439
3440 import bz2
3441 from cStringIO import StringIO
3442-import cElementTree as etree
3443+try:
3444+ import xml.etree.cElementTree as etree
3445+except ImportError:
3446+ import cElementTree as etree
3447 from datetime import datetime, timedelta
3448 from logging import getLogger
3449 import os
3450@@ -437,16 +440,111 @@
3451 aliases.append(alias)
3452 return aliases
3453
3454- _parse_hardware_section = {
3455- 'hal': _parseHAL,
3456- 'processors': _parseProcessors,
3457- 'aliases': _parseAliases}
3458+ def _parseUdev(self, udev_node):
3459+ """Parse the <udev> node.
3460+
3461+ :return: A list of dictionaries, where each dictionary
3462+ describes a udev device.
3463+
3464+ The <udev> node contains the output produced by
3465+ "udevadm info --export-db". Each entry of the dictionaries
3466+ represents the data of the key:value pairs as they appear
3467+ in this data. The value of d['S'] is a list of strings,
3468+ the value s['E'] is a dictionary containing the key=value
3469+ pairs of the "E:" lines.
3470+ """
3471+ # We get the plain text as produced by "udevadm info --export-db"
3472+ # This data looks like:
3473+ #
3474+ # P: /devices/LNXSYSTM:00
3475+ # E: UDEV_LOG=3
3476+ # E: DEVPATH=/devices/LNXSYSTM:00
3477+ # E: MODALIAS=acpi:LNXSYSTM:
3478+ #
3479+ # P: /devices/LNXSYSTM:00/ACPI_CPU:00
3480+ # E: UDEV_LOG=3
3481+ # E: DEVPATH=/devices/LNXSYSTM:00/ACPI_CPU:00
3482+ # E: DRIVER=processor
3483+ # E: MODALIAS=acpi:ACPI_CPU:
3484+ #
3485+ # Data for different devices is separated by empty lines.
3486+ # Each line for a device consists of key:value pairs.
3487+ # The following keys are defined:
3488+ #
3489+ # A: udev_device_get_num_fake_partitions()
3490+ # E: udev_device_get_properties_list_entry()
3491+ # L: the device link priority (udev_device_get_devlink_priority())
3492+ # N: the device node file name (udev_device_get_devnode())
3493+ # P: the device path (udev_device_get_devpath())
3494+ # R: udev_device_get_ignore_remove()
3495+ # S: udev_get_dev_path()
3496+ # W: udev_device_get_watch_handle()
3497+ #
3498+ # The key P is always present; the keys A, L, N, R, W appear at
3499+ # most once per device; the keys E and S may appear more than
3500+ # once.
3501+ # The values of the E records have the format "key=value"
3502+ #
3503+ # See also the libudev reference manual:
3504+ # http://www.kernel.org/pub/linux/utils/kernel/hotplug/libudev/
3505+ # and the udev file udevadm-info.c, function print_record()
3506+
3507+ udev_data = udev_node.text.split('\n')
3508+ devices = []
3509+ device = None
3510+ line_number = 0
3511+
3512+ for line_number, line in enumerate(udev_data):
3513+ if len(line) == 0:
3514+ device = None
3515+ continue
3516+ record = line.split(':', 1)
3517+ if len(record) != 2:
3518+ self._logError(
3519+ 'Line %i in <udev>: No valid key:value data: %r'
3520+ % (line_number, line),
3521+ self.submission_key)
3522+ return None
3523+
3524+ key, value = record
3525+ if device is None:
3526+ device = {
3527+ 'E': {},
3528+ 'S': [],
3529+ }
3530+ devices.append(device)
3531+ # Some attribute lines have a space character after the
3532+ # ':', others don't have it (see udevadm-info.c).
3533+ value = value.lstrip()
3534+
3535+ if key == 'E':
3536+ property_data = value.split('=', 1)
3537+ if len(property_data) != 2:
3538+ self._logError(
3539+ 'Line %i in <udev>: Property without valid key=value '
3540+ 'data: %r' % (line_number, line),
3541+ self.submission_key)
3542+ return None
3543+ property_key, property_value = property_data
3544+ device['E'][property_key] = property_value
3545+ elif key == 'S':
3546+ device['S'].append(value)
3547+ else:
3548+ if key in device:
3549+ self._logWarning(
3550+ 'Line %i in <udev>: Duplicate attribute key: %r'
3551+ % (line_number, line),
3552+ self.submission_key)
3553+ device[key] = value
3554+ return devices
3555
3556 def _setHardwareSectionParsers(self):
3557 self._parse_hardware_section = {
3558 'hal': self._parseHAL,
3559 'processors': self._parseProcessors,
3560- 'aliases': self._parseAliases}
3561+ 'aliases': self._parseAliases,
3562+ 'udev': self._parseUdev,
3563+ }
3564
3565 def _parseHardware(self, hardware_node):
3566 """Parse the <hardware> part of a submission.
3567
3568=== modified file 'lib/canonical/launchpad/scripts/sftracker.py'
3569--- lib/canonical/launchpad/scripts/sftracker.py 2009-06-25 05:30:52 +0000
3570+++ lib/canonical/launchpad/scripts/sftracker.py 2009-09-04 10:43:39 +0000
3571@@ -34,7 +34,7 @@
3572
3573 # use cElementTree if it is available ...
3574 try:
3575- import xml.elementtree.cElementTree as ET
3576+ import xml.etree.cElementTree as ET
3577 except ImportError:
3578 try:
3579 import cElementTree as ET
3580
3581=== added file 'lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml'
3582--- lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml 1970-01-01 00:00:00 +0000
3583+++ lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml 2009-09-14 09:16:45 +0000
3584@@ -0,0 +1,453 @@
3585+<?xml version="1.0" ?>
3586+<system version="1.0">
3587+
3588+ <!-- summary: generic information about the submission -->
3589+ <summary>
3590+
3591+ <!-- live_cd: Was this submission made on a system running an Ubuntu Live
3592+ CD or on a regular Ubuntu/Linux installation?
3593+ -->
3594+ <live_cd value="False"/>
3595+
3596+ <!-- system_id: A hash of the "system identifier". This value is intended
3597+ to identify the tested computer model; the value should
3598+ be derived from the properties
3599+ system.product, system.vendor of the HAL UDI
3600+ /org/freedesktop/Hal/devices/computer.
3601+ -->
3602+ <system_id value="f982bb1ab536469cebfd6eaadcea0ffc"/>
3603+
3604+ <!-- distribution, distroseries: These values are retrieved from
3605+ /etc/lsb-release, parameters DISTRIB_ID and DISTRIB_RELEASE.
3606+ -->
3607+ <distribution value="Ubuntu"/>
3608+ <distroseries value="7.04"/>
3609+
3610+ <!-- architecture: The processor architecture of the operating system.
3611+ -->
3612+ <architecture value="amd64"/>
3613+
3614+ <!-- private: If False, this submission is publicly accessible from
3615+ Launchpad, else it is only accesible by the submitter, by
3616+ Launchpad administrators and by scripts running with
3617+ administrator rights. Submissions marked "private" should
3618+ only be used to gather statistical data.
3619+ -->
3620+ <private value="False"/>
3621+
3622+ <!-- contactable: If True, the owner agrees to be contacted by other
3623+ persons about devices which appear in his submission.
3624+ Example of a use case: Developers can ask device owners
3625+ to perform tests.
3626+ -->
3627+ <contactable value="False"/>
3628+
3629+ <!-- date_created: Date and time (UTC) of the submission.
3630+ -->
3631+ <date_created value="2007-09-28T16:09:20.126842"/>
3632+
3633+ <!-- client: The name and version of the program that created the
3634+ submission data.
3635+ -->
3636+ <client name="hwtest" version="0.9">
3637+
3638+ <!-- plugin: name and version of a plugin used by the client.
3639+ This tag may appear more than once.
3640+ -->
3641+ <plugin name="architecture_info" version="1.1"/>
3642+ <plugin name="find_network_controllers" version="2.34"/>
3643+ <plugin name="internet_ping" version="1.1"/>
3644+ <plugin name="harddisk_speed" version="0.7"/>
3645+ </client>
3646+
3647+ <!-- The kernel name and version, as shown by "uname -r"
3648+ -->
3649+ <kernel-release value="2.6.28-14-generic"/>
3650+ </summary>
3651+
3652+ <!-- hardware: data about the hardware the submission was made on.
3653+ -->
3654+ <hardware>
3655+
3656+ <!-- udev: The output of running "udevadm info - -export-db" -->
3657+
3658+ <udev>P: /devices/LNXSYSTM:00
3659+E: UDEV_LOG=3
3660+E: DEVPATH=/devices/LNXSYSTM:00
3661+E: MODALIAS=acpi:LNXSYSTM:
3662+
3663+P: /devices/pci0000:00/0000:00:1a.0
3664+E: UDEV_LOG=3
3665+E: DEVPATH=/devices/pci0000:00/0000:00:1a.0
3666+E: DRIVER=uhci_hcd
3667+E: PCI_CLASS=C0300
3668+E: PCI_ID=8086:2834
3669+E: PCI_SUBSYS_ID=17AA:20AA
3670+E: PCI_SLOT_NAME=0000:00:1a.0
3671+E: MODALIAS=pci:v00008086d00002834sv000017AAsd000020AAbc0Csc03i00
3672+
3673+P: /devices/pci0000:00/0000:00:1a.0/usb3
3674+N: bus/usb/003/001
3675+S: char/189:256
3676+E: UDEV_LOG=3
3677+E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3
3678+E: MAJOR=189
3679+E: MINOR=256
3680+E: DEVTYPE=usb_device
3681+E: DRIVER=usb
3682+E: DEVICE=/proc/bus/usb/003/001
3683+E: PRODUCT=1d6b/1/206
3684+E: TYPE=9/0/0
3685+E: BUSNUM=003
3686+E: DEVNUM=001
3687+E: DEVNAME=/dev/bus/usb/003/001
3688+E: DEVLINKS=/dev/char/189:256
3689+
3690+P: /devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0
3691+E: UDEV_LOG=3
3692+E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0
3693+E: DEVTYPE=usb_interface
3694+E: DRIVER=hub
3695+E: DEVICE=/proc/bus/usb/003/001
3696+E: PRODUCT=1d6b/1/206
3697+E: TYPE=9/0/0
3698+E: INTERFACE=9/0/0
3699+E: MODALIAS=usb:v1D6Bp0001d0206dc09dsc00dp00ic09isc00ip00
3700+
3701+P: /devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81
3702+N: usbdev3.1_ep81
3703+S: char/252:4
3704+E: UDEV_LOG=3
3705+E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81
3706+E: MAJOR=252
3707+E: MINOR=4
3708+E: DEVNAME=/dev/usbdev3.1_ep81
3709+E: DEVLINKS=/dev/char/252:4
3710+
3711+P: /devices/pci0000:00/0000:00:1f.1
3712+E: UDEV_LOG=3
3713+E: DEVPATH=/devices/pci0000:00/0000:00:1f.1
3714+E: DRIVER=ata_piix
3715+E: PCI_CLASS=1018A
3716+E: PCI_ID=8086:2850
3717+E: PCI_SUBSYS_ID=17AA:20A6
3718+E: PCI_SLOT_NAME=0000:00:1f.1
3719+E: MODALIAS=pci:v00008086d00002850sv000017AAsd000020A6bc01sc01i8a
3720+
3721+P: /devices/pci0000:00/0000:00:1f.1/host3
3722+E: UDEV_LOG=3
3723+E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3
3724+E: DEVTYPE=scsi_host
3725+
3726+P: /devices/pci0000:00/0000:00:1f.1/host3/scsi_host/host3
3727+E: UDEV_LOG=3
3728+E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/scsi_host/host3
3729+
3730+P: /devices/pci0000:00/0000:00:1f.1/host3/target3:0:0
3731+E: UDEV_LOG=3
3732+E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0
3733+E: DEVTYPE=scsi_target
3734+
3735+P: /devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0
3736+E: UDEV_LOG=3
3737+E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0
3738+E: DEVTYPE=scsi_device
3739+E: DRIVER=sr
3740+E: MODALIAS=scsi:t-0x05
3741+</udev>
3742+
3743+ <!-- The content of publicly accessible files in /sys/class/dmi/id/
3744+ format: filename:content
3745+ as for example generated by "grep -r . /sys/class/dmi/id/"
3746+ -->
3747+
3748+ <dmi>/sys/class/dmi/id/bios_vendor:LENOVO
3749+/sys/class/dmi/id/bios_version:7LETB9WW (2.19 )
3750+/sys/class/dmi/id/bios_date:06/06/2008
3751+/sys/class/dmi/id/sys_vendor:LENOVO
3752+/sys/class/dmi/id/product_name:6457BAG
3753+/sys/class/dmi/id/product_version:ThinkPad T61
3754+/sys/class/dmi/id/board_vendor:LENOVO
3755+/sys/class/dmi/id/board_name:6457BAG
3756+/sys/class/dmi/id/board_version:Not Available
3757+/sys/class/dmi/id/chassis_vendor:LENOVO
3758+/sys/class/dmi/id/chassis_type:10
3759+/sys/class/dmi/id/chassis_version:Not Available
3760+/sys/class/dmi/id/chassis_asset_tag:No Asset Information
3761+/sys/class/dmi/id/modalias:dmi:bvnLENOVO:bvr7LETB9WW(2.19)
3762+</dmi>
3763+
3764+ <!-- Additional data for SCSI devices: vendor, model, type
3765+
3766+ For each udev node which has DEVTYPE=scsi_device, we need
3767+ the content of the sysfs files vendor, model, type. The data
3768+ is stored in the same format as the DMI data:
3769+ /path/to/file:filecontent
3770+ -->
3771+ <sysfs-attributes>/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/vendor:HL-DT-ST
3772+/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/model:DVDRAM GSA-4083N
3773+/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/type:5
3774+ </sysfs-attributes>
3775+
3776+ <!-- processors: Data about processors installed in a system.
3777+ The data is retrieved from /proc/cpuinfo.
3778+ -->
3779+ <processors>
3780+
3781+ <!-- processor: Data from /proc/cpuinfo about a single processor.
3782+ -->
3783+ <processor id="123" name="0">
3784+
3785+ <!-- property: The data of one line of /proc/cpuinfo.
3786+ attribute name: The name of the property
3787+ (the text left of the ':' in a line of /proc/cpuinfo)
3788+ attribute type: A Python type appropriate for the value.
3789+ -->
3790+ <property name="wp" type="bool">
3791+ True
3792+ </property>
3793+ <property name="flags" type="list">
3794+ <value type="str">
3795+ fpu
3796+ </value>
3797+ <value type="str">
3798+ vme
3799+ </value>
3800+ <value type="str">
3801+ de
3802+ </value>
3803+ </property>
3804+ <property name="cpu_mhz" type="float">
3805+ 1000.0
3806+ </property>
3807+ </processor>
3808+ </processors>
3809+
3810+ <!-- aliases: optional data provided by the user:
3811+ The name of a peripheral, PCI card etc as shown by a label on
3812+ the device. OEM devices are often sold under different names
3813+ by different vendors; having a set of alias names for a device
3814+ allows users of the HWDB to search for information by these
3815+ "marketing names".
3816+ -->
3817+ <aliases>
3818+ <!-- alias: The "label name" of a device or system.
3819+ attribute target: The sysfs path of a device as given
3820+ in <udev>.
3821+ -->
3822+ <alias target="/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0">
3823+
3824+ <!-- vendor: The vendor name shown on the device label.
3825+ -->
3826+ <vendor>Medion</vendor>
3827+
3828+ <!-- model: The model name of shown on the label.
3829+ -->
3830+ <model>QuickPrint 9876</model>
3831+ </alias>
3832+ </aliases>
3833+ </hardware>
3834+
3835+ <!-- software: Data about the software installed on the system.
3836+ -->
3837+ <software>
3838+
3839+ <!-- lsbrelease: The data from /etc/lsb-release.
3840+ -->
3841+ <lsbrelease>
3842+
3843+ <!-- property: the data from one line of /etc/lsb-release.
3844+ attribute type: A Python type appropriate for this
3845+ property (str).
3846+ -->
3847+ <property name="release" type="str">
3848+ 7.04
3849+ </property>
3850+ <property name="codename" type="str">
3851+ feisty
3852+ </property>
3853+ <property name="distributor-id" type="str">
3854+ Ubuntu
3855+ </property>
3856+ <property name="description" type="str">
3857+ Ubuntu 7.04
3858+ </property>
3859+ <property name="dict_example" type="dict">
3860+ <value name="a" type="str">value for key a</value>
3861+ <value name="b" type="int">1234</value>
3862+ </property>
3863+ </lsbrelease>
3864+
3865+ <!-- packages: Data about the installed software packages.
3866+ -->
3867+ <packages>
3868+
3869+ <!-- package: Data about a single package.
3870+ The <property> sub-tags contain the DEB properties
3871+ "name", "priority", "section", "source", "version",
3872+ "installed_size", "size", "summary".
3873+
3874+ XXX Abel Deuring 2007-12-12: What about submissions
3875+ from RPM-based Linux versions? (And "exotic" variants
3876+ like Gentoo?)
3877+ -->
3878+ <package name="metacity" id="200">
3879+ <property name="installed_size" type="int">
3880+ 868352
3881+ </property>
3882+ <property name="section" type="str">
3883+ x11
3884+ </property>
3885+ <property name="summary" type="str">
3886+ A lightweight GTK2 based Window Manager
3887+ </property>
3888+ <property name="priority" type="str">
3889+ optional
3890+ </property>
3891+ <property name="source" type="str">
3892+ metacity
3893+ </property>
3894+ <property name="version" type="str">
3895+ 1:2.18.2-0ubuntu1.1
3896+ </property>
3897+ <property name="size" type="int">
3898+ 429128
3899+ </property>
3900+ </package>
3901+ </packages>
3902+ <!-- Information extracted from Xorg.0.log.
3903+ HAL does not provide any information about Xorg drivers, so
3904+ we retrieve that from the xserver's log file.
3905+ -->
3906+ <xorg version="1.3.0">
3907+ <!-- driver: Data about a driver.
3908+ (optional)
3909+ attribute name: The name of the driver.
3910+ attribute version: The version of the driver.
3911+ attribute class: The module class of the driver
3912+ attribute device: The ID of a device driven by this driver.
3913+ -->
3914+ <driver name="fglrx" version="1.23" class="X.Org Video Driver"
3915+ device="12"/>
3916+ </xorg>
3917+ </software>
3918+
3919+ <!-- questions: User's answers to questions asked by the client.
3920+ -->
3921+ <questions>
3922+
3923+ <!-- question: Data of a question.
3924+ attribute name: The unique name of the question.
3925+ attribute plugin: The name of the plugin which asked
3926+ the question.
3927+ attribute version: The version of the question.
3928+ attribute type: Allowed values are "manual" and "automatic".
3929+ A "manual" question requires user input for the answer;
3930+ an "automatic" question gets the answer automatically.
3931+ -->
3932+ <question name="detected_network_controllers"
3933+ plugin="find_network_controllers">
3934+
3935+ <!-- target: Information about a device or software package the
3936+ question is about. The attribute "id" is the sysfs path of
3937+ a device as given in <udev>, or the ID of a software package
3938+ node.
3939+ This node may appear multiple times.
3940+ -->
3941+ <target id="/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81">
3942+ <!-- driver: The driver which controls the target device. This tag
3943+ may appear more than once.
3944+
3945+ While we are working on a project called "Hardware Database",
3946+ we are not that much interested in the question, if a device
3947+ works "as such", but if their Linux driver(s) work.
3948+
3949+ It is not in every case possible to identify the used driver
3950+ from HAL data, so we need another way to add this information.
3951+ (example: HAL does not know, which driver is used for the
3952+ graphics card.)
3953+
3954+ Example for multiple drivers: Some scanners have a SCSI _and_
3955+ a USB interface; if such a scanner is tested, we not only want
3956+ to know, which Sane backend is used, but also, which interface
3957+ is used.
3958+
3959+ Also, it might be interesting to know for many USB 2.0 devices,
3960+ if a USB 1 (uhci_hcd or ohci_hcd driver) or the USB 2 driver
3961+ (ehci_hcd) was used. A USB 1 driver might for example explain
3962+ latency problems.
3963+ -->
3964+ <driver>ipw3945</driver>
3965+ </target>
3966+
3967+ <!-- ID of the 88E8055 PCI-E Gigabit Ethernet Controller -->
3968+ <target id="/devices/pci0000:00/0000:00:1f.1"/>
3969+
3970+ <!-- command: The command line of an external command required to
3971+ ask this question.
3972+ -->
3973+ <command/>
3974+
3975+ <!-- answer: The answer to the question. Two types of answers are
3976+ defined, "multiple_choice" and "measurement". (See below
3977+ for an example of the latter.)
3978+ attribute type: Must be "multiple_choice" or "measurement".
3979+ -->
3980+ <answer type="multiple_choice">pass</answer>
3981+
3982+ <!-- answer_choices: The list of possible choices.
3983+ The data should only be used for consistency
3984+ checks and to detect variants of the question.
3985+ -->
3986+ <answer_choices>
3987+ <value type="str">fail</value>
3988+ <value type="str">pass</value>
3989+ <value type="str">skip</value>
3990+ </answer_choices>
3991+
3992+ <!-- A user comment about the device or about the test.
3993+ -->
3994+ <comment>
3995+ The WLAN adapter drops the connection very frequently.
3996+ </comment>
3997+ </question>
3998+
3999+ <question name="internet_ping"
4000+ plugin="internet_ping">
4001+ <target id="/devices/pci0000:00/0000:00:1f.1"/>
4002+ <command/>
4003+ <answer type="multiple_choice">pass</answer>
4004+ <answer_choices>
4005+ <value type="str">fail</value>
4006+ <value type="str">pass</value>
4007+ <value type="str">skip</value>
4008+ </answer_choices>
4009+ </question>
4010+
4011+ <!-- example for a "measurement question"
4012+ -->
4013+ <question name="harddisk_speed"
4014+ plugin="harddisk_speed">
4015+ <target id="/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0"/>
4016+ <command>hdparm -t /dev/sda</command>
4017+ <!-- answer: The answer to a "measurement question".
4018+ attribute type: See above.
4019+ attribute unit: The unit of the result of the measurement.
4020+ XXX Abel Deuring 2007-12-12 bug=175978 We should
4021+ enumerate a list of allowed units, in order to avoid
4022+ multiple units for the same dimension. e.g., B/sec,
4023+ MB/sec or inch, cm, foot.
4024+
4025+ For dimensionless values, the attribute unit is omitted.
4026+
4027+ "Percentage" and similar "convenience pseudo-units" like
4028+ ppm are _not_ allowed; instead a dimensionless
4029+ value must be used, where 0 is equivalent 0% and 1.0 is
4030+ equivalent to 100%.
4031+ -->
4032+ <answer type="measurement" unit="MB/sec">38.4</answer>
4033+ </question>
4034+ </questions>
4035+ <!-- miscellaneous additional text data.
4036+ -->
4037+</system>
4038
4039=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py'
4040--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-06-25 05:30:52 +0000
4041+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-09-15 17:05:32 +0000
4042@@ -4,7 +4,10 @@
4043 """Tests of the HWDB submissions parser."""
4044
4045 from cStringIO import StringIO
4046-import cElementTree as etree
4047+try:
4048+ import xml.etree.cElementTree as etree
4049+except ImportError:
4050+ import cElementTree as etree
4051 from datetime import datetime
4052 import logging
4053 import os
4054@@ -507,6 +510,111 @@
4055 'model': 'MD 4394'}],
4056 'Invalid parsing result for <aliases>')
4057
4058+ def testUdev(self):
4059+ """The content of the <udev> node is converted into a list of dicts.
4060+ """
4061+ parser = SubmissionParser(self.log)
4062+ node = etree.fromstring("""
4063+<udev>P: /devices/LNXSYSTM:00
4064+E: UDEV_LOG=3
4065+E: DEVPATH=/devices/LNXSYSTM:00
4066+E: MODALIAS=acpi:LNXSYSTM:
4067+
4068+P: /devices/pci0000:00/0000:00:1a.0
4069+E: UDEV_LOG=3
4070+E: DEVPATH=/devices/pci0000:00/0000:00:1a.0
4071+S: char/189:256
4072+</udev>
4073+""")
4074+ result = parser._parseUdev(node)
4075+ self.assertEqual(
4076+ [
4077+ {
4078+ 'P': '/devices/LNXSYSTM:00',
4079+ 'E': {
4080+ 'UDEV_LOG': '3',
4081+ 'DEVPATH': '/devices/LNXSYSTM:00',
4082+ 'MODALIAS': 'acpi:LNXSYSTM:',
4083+ },
4084+ 'S': [],
4085+ },
4086+ {
4087+ 'P': '/devices/pci0000:00/0000:00:1a.0',
4088+ 'E': {
4089+ 'UDEV_LOG': '3',
4090+ 'DEVPATH': '/devices/pci0000:00/0000:00:1a.0',
4091+ },
4092+ 'S': ['char/189:256'],
4093+ },
4094+ ],
4095+ result,
4096+ 'Invalid parsing result for <udev>')
4097+
4098+ def testUdevLineWithoutColon(self):
4099+ """<udev> nodes with lines not in key: value format are rejected."""
4100+ parser = SubmissionParser(self.log)
4101+ parser.submission_key = 'Detect udev lines not in key:value format'
4102+ node = etree.fromstring("""
4103+<udev>P: /devices/LNXSYSTM:00
4104+bad line
4105+</udev>
4106+""")
4107+ result = parser._parseUdev(node)
4108+ self.assertEqual(
4109+ None, result,
4110+ 'Invalid parsing result for a <udev> node with a line not having '
4111+ 'the key: value format.')
4112+ self.assertErrorMessage(
4113+ parser.submission_key,
4114+ "Line 1 in <udev>: No valid key:value data: 'bad line'")
4115+
4116+ def testUdevPropertyLineWithoutEqualSign(self):
4117+ """<udev> nodes with lines not in key: value format are rejected."""
4118+ parser = SubmissionParser(self.log)
4119+ parser.submission_key = (
4120+ 'Detect udev property lines not in key=value format')
4121+ node = etree.fromstring("""
4122+<udev>P: /devices/LNXSYSTM:00
4123+E: bad property
4124+</udev>
4125+""")
4126+ result = parser._parseUdev(node)
4127+ self.assertEqual(
4128+ None, result,
4129+ 'Invalid parsing result for a <udev> node with a property line '
4130+ 'not having the key=value format.')
4131+ self.assertErrorMessage(
4132+ parser.submission_key,
4133+ "Line 1 in <udev>: Property without valid key=value data: "
4134+ "'E: bad property'")
4135+
4136+ def testUdevDataWithDuplicateKey(self):
4137+ """<udev> nodes with lines not in key: value format are rejected."""
4138+ parser = SubmissionParser(self.log)
4139+ parser.submission_key = 'Detect duplactae attributes in udev data'
4140+ node = etree.fromstring("""
4141+<udev>P: /devices/LNXSYSTM:00
4142+W:1
4143+W:2
4144+</udev>
4145+""")
4146+ result = parser._parseUdev(node)
4147+ self.assertEqual(
4148+ [
4149+ {
4150+ 'P': '/devices/LNXSYSTM:00',
4151+ 'E': {},
4152+ 'S': [],
4153+ 'W': '2',
4154+ },
4155+ ],
4156+ result,
4157+ 'Invalid parsing result for a <udev> node with a duplicate '
4158+ 'attribute.')
4159+ self.assertWarningMessage(
4160+ parser.submission_key,
4161+ "Line 2 in <udev>: Duplicate attribute key: 'W:2'")
4162+
4163 def testHardware(self):
4164 """The <hardware> tag is converted into a dictionary."""
4165 test = self
4166
4167=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py'
4168--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py 2009-06-25 05:30:52 +0000
4169+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py 2009-09-14 17:38:57 +0000
4170@@ -375,8 +375,20 @@
4171
4172 The only allowed tags are specified by the Relax NG schema:
4173 live_cd, system_id, distribution, distroseries, architecture,
4174- private, contactable, date_created.
4175+ private, contactable, date_created (tested in
4176+ testSummaryRequiredTags()), and the optional tag <kernel-release>.
4177 """
4178+ # we can add the tag <kernel-release>
4179+ sample_data = self.insertSampledata(
4180+ data=self.sample_data,
4181+ insert_text='<kernel-release value="2.6.28-15-generic"/>',
4182+ where='</summary>')
4183+ result, submission_id = self.runValidator(sample_data)
4184+ self.assertNotEqual(
4185+ result, None,
4186+ 'Valid submission containing a <kernel-release> tag rejected.')
4187+
4188+ # Adding any other tag is not possible.
4189 sample_data = self.insertSampledata(
4190 data=self.sample_data,
4191 insert_text='<nonsense/>',
4192@@ -609,24 +621,99 @@
4193 'Invalid attribute foo for element plugin',
4194 'invalid attribute in client plugin')
4195
4196- def testHardwareSubTags(self):
4197+ def testHardwareSubTagHalOrUdev(self):
4198+ """The <hardware> tag requires data about hardware devices.
4199+
4200+ This data is stored either in the sub-tag <hal> or in the
4201+ three tags <udev>, <dmi>, <sysfs-attributes>.
4202+ """
4203+ # Omitting <hal> leads to an error.
4204+ sample_data = self.replaceSampledata(
4205+ data=self.sample_data,
4206+ replace_text='',
4207+ from_text='<hal',
4208+ to_text='</hal>')
4209+ result, submission_id = self.runValidator(sample_data)
4210+ self.assertErrorMessage(
4211+ submission_id, result,
4212+ 'Expecting an element hal, got nothing',
4213+ 'missing tag <hal> in <hardware>')
4214+
4215+ # But we may replace <hal> by the three tags <udev>, <dmi>,
4216+ #<sysfs-attributes>.
4217+ sample_data = self.replaceSampledata(
4218+ data=self.sample_data,
4219+ replace_text="""
4220+ <udev>some text</udev>
4221+ <dmi>some text</dmi>
4222+ <sysfs-attributes>some text</sysfs-attributes>
4223+ """,
4224+ from_text='<hal',
4225+ to_text='</hal>')
4226+ result, submission_id = self.runValidator(sample_data)
4227+ self.assertNotEqual(
4228+ result, None,
4229+ 'submission with valid <udev>, <dmi>, <sysfs-attributes> tags '
4230+ 'rejected')
4231+
4232+ def testHardwareSubTagUdevIncomplete(self):
4233 """The <hardware> tag has a fixed set of allowed sub-tags.
4234
4235- Valid sub-tags are <hal>, <processors>, <aliases>.
4236- <aliases> is optional; <hal> and <processors> are required.
4237+ Valid sub-tags are <hal>, <udev>, <dmi>, <sysfs-attributes>,
4238+ <processors>, <aliases>. <aliases> is optional, <processors>
4239+ is required, and either <hal> or all three tags <udev>, <dmi>,
4240+ <sysfs-attributes> must be present.
4241 """
4242- # Omitting either of the required tags leads on an error.
4243- for tag in ('hal', 'processors'):
4244+ # Omitting any of the three tags <udev>, <dmi>, <sysfs-attributes>
4245+ # makes the data invalid.
4246+ all_tags = ['udev', 'dmi', 'sysfs-attributes']
4247+ for index, missing_tag in enumerate(all_tags):
4248+ test_tags = all_tags[:]
4249+ del test_tags[index]
4250+ replace_text = [
4251+ '<%s>text</%s>' % (tag, tag) for tag in test_tags]
4252+ replace_text = ''.join(replace_text)
4253 sample_data = self.replaceSampledata(
4254 data=self.sample_data,
4255- replace_text='',
4256- from_text='<%s' % tag,
4257- to_text='</%s>' % tag)
4258- result, submission_id = self.runValidator(sample_data)
4259- self.assertErrorMessage(
4260- submission_id, result,
4261- 'Expecting an element %s, got nothing' % tag,
4262- 'missing tag <%s> in <hardware>' % tag)
4263+ replace_text=replace_text,
4264+ from_text='<hal',
4265+ to_text='</hal>')
4266+ result, submission_id = self.runValidator(sample_data)
4267+ self.assertErrorMessage(
4268+ submission_id, result,
4269+ 'Expecting an element %s, got nothing' % missing_tag,
4270+ 'missing tag <%s> in <hardware>' % missing_tag)
4271+
4272+ def testHardwareSubTagHalMixedWithUdev(self):
4273+ """Mixing <hal> with <udev>, <dmi>, <sysfs-attributes> is impossible.
4274+ """
4275+ # A submission containing the tag <hal> as well as one of <udev>,
4276+ # <dmi>, <sysfs-attributes> is invalid.
4277+ for tag in ['udev', 'dmi', 'sysfs-attributes']:
4278+ sample_data = self.insertSampledata(
4279+ data=self.sample_data,
4280+ insert_text='<%s>some text</%s>' % (tag, tag),
4281+ where='<hal')
4282+ result, submission_id = self.runValidator(sample_data)
4283+ self.assertErrorMessage(
4284+ submission_id, result,
4285+ 'Invalid sequence in interleave',
4286+ '<hal> mixed with <%s> in <hardware>' % tag)
4287+
4288+ def testHardwareOtherSubTags(self):
4289+ """The <hardware> tag has a fixed set of allowed sub-tags.
4290+ """
4291+ # The <processors> tag must not be omitted.
4292+ sample_data = self.replaceSampledata(
4293+ data=self.sample_data,
4294+ replace_text='',
4295+ from_text='<processors',
4296+ to_text='</processors>')
4297+ result, submission_id = self.runValidator(sample_data)
4298+ self.assertErrorMessage(
4299+ submission_id, result,
4300+ 'Expecting an element processors, got nothing',
4301+ '<processor> tag omitted')
4302
4303 # The <aliases> tag may be omitted.
4304 sample_data = self.replaceSampledata(
4305@@ -1603,24 +1690,6 @@
4306 'Extra element aliases in interleave',
4307 'missing attribute of <alias>')
4308
4309- # target must be an integer.
4310- sample_data = self.sample_data.replace(
4311- '<alias target="65">', '<alias target="noInteger">')
4312- result, submission_id = self.runValidator(sample_data)
4313- self.assertErrorMessage(
4314- submission_id, result,
4315- 'Extra element aliases in interleave',
4316- 'missing attribute of <alias>')
4317-
4318- # target must not be empty.
4319- sample_data = self.sample_data.replace(
4320- '<alias target="65">', '<alias target="">')
4321- result, submission_id = self.runValidator(sample_data)
4322- self.assertErrorMessage(
4323- submission_id, result,
4324- 'Element hardware failed to validate content',
4325- 'missing attribute of <alias>')
4326-
4327 # Other attributes are not allowed. We get again the same
4328 # quite unspecific error message as above.
4329 sample_data = self.sample_data.replace(
4330
4331=== modified file 'lib/canonical/launchpad/security.py'
4332--- lib/canonical/launchpad/security.py 2009-08-20 12:36:07 +0000
4333+++ lib/canonical/launchpad/security.py 2009-08-31 03:03:00 +0000
4334@@ -63,7 +63,7 @@
4335 IMilestone, IProjectMilestone)
4336 from canonical.launchpad.interfaces.oauth import (
4337 IOAuthAccessToken, IOAuthRequestToken)
4338-from lp.soyuz.interfaces.packageset import IPackagesetSet
4339+from lp.soyuz.interfaces.packageset import IPackageset, IPackagesetSet
4340 from lp.translations.interfaces.pofile import IPOFile
4341 from lp.translations.interfaces.potemplate import (
4342 IPOTemplate, IPOTemplateSubset)
4343@@ -195,6 +195,7 @@
4344 return (user.inTeam(celebrities.registry_experts)
4345 or user.inTeam(celebrities.admin))
4346
4347+
4348 class ReviewProduct(ReviewByRegistryExpertsOrAdmins):
4349 usedfor = IProduct
4350
4351@@ -211,8 +212,6 @@
4352 usedfor = IProjectSet
4353
4354
4355-
4356-
4357 class ViewPillar(AuthorizationBase):
4358 usedfor = IPillar
4359 permission = 'launchpad.View'
4360@@ -385,8 +384,7 @@
4361 if user.inTeam(driver):
4362 return True
4363 admins = getUtility(ILaunchpadCelebrities).admin
4364- return (user.inTeam(self.obj.target.owner) or
4365- user.inTeam(admins))
4366+ return (user.inTeam(targetowner) or user.inTeam(admins))
4367
4368
4369 class DriverSpecification(AuthorizationBase):
4370@@ -514,6 +512,7 @@
4371 """IProjectMilestone is a fake content object."""
4372 return False
4373
4374+
4375 class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
4376 permission = 'launchpad.Edit'
4377 usedfor = IMilestone
4378@@ -2268,6 +2267,18 @@
4379 or user.inTeam(celebrities.admin))
4380
4381
4382+class EditPackageset(AuthorizationBase):
4383+ permission = 'launchpad.Edit'
4384+ usedfor = IPackageset
4385+
4386+ def checkAuthenticated(self, user):
4387+ """The owner of a package set can edit the object."""
4388+ celebrities = getUtility(ILaunchpadCelebrities)
4389+ return (
4390+ user.inTeam(self.obj.owner)
4391+ or user.inTeam(celebrities.admin))
4392+
4393+
4394 class EditPackagesetSet(AuthorizationBase):
4395 permission = 'launchpad.Edit'
4396 usedfor = IPackagesetSet
4397
4398=== removed file 'lib/canonical/launchpad/templates/bugbranch-delete.pt'
4399--- lib/canonical/launchpad/templates/bugbranch-delete.pt 2009-07-17 17:59:07 +0000
4400+++ lib/canonical/launchpad/templates/bugbranch-delete.pt 1970-01-01 00:00:00 +0000
4401@@ -1,28 +0,0 @@
4402-<html
4403- xmlns="http://www.w3.org/1999/xhtml"
4404- xml:lang="en"
4405- lang="en"
4406- metal:use-macro="context/@@main_template/master"
4407- i18n:domain="launchpad">
4408-
4409-<body>
4410-
4411-<metal:leftportlets fill-slot="portlets">
4412- <div tal:replace="structure context/branch/@@+portlet-details" />
4413-</metal:leftportlets>
4414-
4415-<div metal:fill-slot="main">
4416-
4417- <h1>Delete bug branch link</h1>
4418-
4419- <div class="documentDescription">
4420- Are you sure you want to remove the link between...
4421- </div>
4422-
4423- <div metal:use-macro="context/@@launchpad_form/form">
4424- </div>
4425-
4426-</div>
4427-
4428-</body>
4429-</html>
4430
4431=== modified file 'lib/canonical/launchpad/templates/launchpad-form.pt'
4432--- lib/canonical/launchpad/templates/launchpad-form.pt 2009-08-01 02:04:55 +0000
4433+++ lib/canonical/launchpad/templates/launchpad-form.pt 2009-09-03 15:39:22 +0000
4434@@ -14,11 +14,6 @@
4435 enctype="multipart/form-data"
4436 accept-charset="UTF-8">
4437
4438- <h1 tal:condition="view/label"
4439- tal:content="view/label"
4440- metal:define-slot="heading"
4441- >Add Something</h1>
4442-
4443 <div metal:define-macro="formbody">
4444
4445 <p metal:define-slot="extra_info" tal:replace="nothing">
4446@@ -38,8 +33,8 @@
4447 Schema validation errors.
4448 </p>
4449
4450- <div class="row"
4451- metal:define-slot="extra_top"
4452+ <div class="row"
4453+ metal:define-slot="extra_top"
4454 tal:replace="nothing">
4455 <div>Extra top</div>
4456 <div><input type="text"/></div>
4457@@ -55,7 +50,7 @@
4458 tal:content="structure script" />
4459
4460 <div class="row"
4461- metal:define-slot="extra_bottom"
4462+ metal:define-slot="extra_bottom"
4463 tal:replace="nothing">
4464 <div>Extra bottom</div>
4465 <div class="field"><input type="text" /></div>
4466@@ -73,8 +68,8 @@
4467 </tal:has-cancel-link>
4468 </div>
4469
4470- <div class="row"
4471- metal:define-slot="extra_buttons"
4472+ <div class="row"
4473+ metal:define-slot="extra_buttons"
4474 tal:replace="nothing">
4475 </div>
4476
4477
4478=== modified file 'lib/canonical/launchpad/templates/logintoken-claimprofile.pt'
4479--- lib/canonical/launchpad/templates/logintoken-claimprofile.pt 2009-07-17 17:59:07 +0000
4480+++ lib/canonical/launchpad/templates/logintoken-claimprofile.pt 2009-09-16 20:51:57 +0000
4481@@ -3,10 +3,7 @@
4482 xmlns:tal="http://xml.zope.org/namespaces/tal"
4483 xmlns:metal="http://xml.zope.org/namespaces/metal"
4484 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4485- xml:lang="en"
4486- lang="en"
4487- dir="ltr"
4488- metal:use-macro="context/@@main_template/master"
4489+ metal:use-macro="view/macro:page/locationless"
4490 i18n:domain="launchpad"
4491 >
4492 <body>
4493
4494=== modified file 'lib/canonical/launchpad/templates/logintoken-claimteam.pt'
4495--- lib/canonical/launchpad/templates/logintoken-claimteam.pt 2009-07-17 17:59:07 +0000
4496+++ lib/canonical/launchpad/templates/logintoken-claimteam.pt 2009-09-16 20:51:57 +0000
4497@@ -3,10 +3,7 @@
4498 xmlns:tal="http://xml.zope.org/namespaces/tal"
4499 xmlns:metal="http://xml.zope.org/namespaces/metal"
4500 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4501- xml:lang="en"
4502- lang="en"
4503- dir="ltr"
4504- metal:use-macro="context/@@main_template/master"
4505+ metal:use-macro="view/macro:page/locationless"
4506 i18n:domain="launchpad"
4507 >
4508 <body>
4509
4510=== modified file 'lib/canonical/launchpad/templates/logintoken-index.pt'
4511--- lib/canonical/launchpad/templates/logintoken-index.pt 2009-07-17 17:59:07 +0000
4512+++ lib/canonical/launchpad/templates/logintoken-index.pt 2009-09-17 18:46:13 +0000
4513@@ -3,10 +3,7 @@
4514 xmlns:tal="http://xml.zope.org/namespaces/tal"
4515 xmlns:metal="http://xml.zope.org/namespaces/metal"
4516 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4517- xml:lang="en"
4518- lang="en"
4519- dir="ltr"
4520- metal:use-macro="context/@@main_template/master"
4521+ metal:use-macro="view/macro:page/locationless"
4522 i18n:domain="launchpad"
4523 >
4524
4525@@ -14,11 +11,9 @@
4526
4527 <div metal:fill-slot="main">
4528
4529- <h1>Confirmation already concluded</h1>
4530-
4531 <p>
4532 You reached this page probably because you followed a link received by
4533- email. That link was sent to confirm you have access to the email
4534+ email. That link was sent to confirm you have access to the email
4535 address it was sent to, but this confirmation was already concluded, so
4536 you don't need to do anything else.
4537 </p>
4538
4539=== modified file 'lib/canonical/launchpad/templates/logintoken-newaccount.pt'
4540--- lib/canonical/launchpad/templates/logintoken-newaccount.pt 2009-07-17 17:59:07 +0000
4541+++ lib/canonical/launchpad/templates/logintoken-newaccount.pt 2009-09-16 20:51:57 +0000
4542@@ -3,10 +3,7 @@
4543 xmlns:tal="http://xml.zope.org/namespaces/tal"
4544 xmlns:metal="http://xml.zope.org/namespaces/metal"
4545 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4546- xml:lang="en"
4547- lang="en"
4548- dir="ltr"
4549- metal:use-macro="context/@@main_template/master"
4550+ metal:use-macro="view/macro:page/locationless"
4551 i18n:domain="launchpad"
4552 >
4553 <body>
4554
4555=== modified file 'lib/canonical/launchpad/templates/logintoken-resetpassword.pt'
4556--- lib/canonical/launchpad/templates/logintoken-resetpassword.pt 2009-07-17 17:59:07 +0000
4557+++ lib/canonical/launchpad/templates/logintoken-resetpassword.pt 2009-09-16 20:51:57 +0000
4558@@ -3,10 +3,7 @@
4559 xmlns:tal="http://xml.zope.org/namespaces/tal"
4560 xmlns:metal="http://xml.zope.org/namespaces/metal"
4561 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4562- xml:lang="en"
4563- lang="en"
4564- dir="ltr"
4565- metal:use-macro="context/@@main_template/master"
4566+ metal:use-macro="view/macro:page/locationless"
4567 i18n:domain="launchpad"
4568 >
4569 <body>
4570
4571=== modified file 'lib/canonical/launchpad/templates/logintoken-validateemail.pt'
4572--- lib/canonical/launchpad/templates/logintoken-validateemail.pt 2009-07-17 17:59:07 +0000
4573+++ lib/canonical/launchpad/templates/logintoken-validateemail.pt 2009-09-16 20:51:57 +0000
4574@@ -3,10 +3,7 @@
4575 xmlns:tal="http://xml.zope.org/namespaces/tal"
4576 xmlns:metal="http://xml.zope.org/namespaces/metal"
4577 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4578- xml:lang="en"
4579- lang="en"
4580- dir="ltr"
4581- metal:use-macro="context/@@main_template/master"
4582+ metal:use-macro="view/macro:page/locationless"
4583 i18n:domain="launchpad"
4584 >
4585 <body>
4586
4587=== modified file 'lib/canonical/launchpad/templates/logintoken-validategpg.pt'
4588--- lib/canonical/launchpad/templates/logintoken-validategpg.pt 2009-07-17 17:59:07 +0000
4589+++ lib/canonical/launchpad/templates/logintoken-validategpg.pt 2009-09-16 20:51:57 +0000
4590@@ -3,10 +3,7 @@
4591 xmlns:tal="http://xml.zope.org/namespaces/tal"
4592 xmlns:metal="http://xml.zope.org/namespaces/metal"
4593 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4594- xml:lang="en"
4595- lang="en"
4596- dir="ltr"
4597- metal:use-macro="context/@@main_template/master"
4598+ metal:use-macro="view/macro:page/locationless"
4599 i18n:domain="launchpad"
4600 >
4601 <body>
4602
4603=== modified file 'lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt'
4604--- lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt 2009-07-17 17:59:07 +0000
4605+++ lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt 2009-09-17 18:46:13 +0000
4606@@ -3,10 +3,7 @@
4607 xmlns:tal="http://xml.zope.org/namespaces/tal"
4608 xmlns:metal="http://xml.zope.org/namespaces/metal"
4609 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4610- xml:lang="en"
4611- lang="en"
4612- dir="ltr"
4613- metal:use-macro="context/@@main_template/master"
4614+ metal:use-macro="view/macro:page/locationless"
4615 i18n:domain="launchpad"
4616 >
4617 <body>
4618@@ -15,10 +12,6 @@
4619
4620 <div metal:use-macro="context/@@launchpad_form/form">
4621
4622- <metal:heading fill-slot="heading">
4623- <h1>Confirm your OpenPGP key</h1>
4624- </metal:heading>
4625-
4626 <p metal:fill-slot="extra_info">
4627 Thanks for adding your OpenPGP key to Launchpad. So we can confirm that the key is yours, we need you to use the key to sign some text.
4628 </p>
4629
4630=== modified file 'lib/canonical/launchpad/templates/logintoken-validateteamemail.pt'
4631--- lib/canonical/launchpad/templates/logintoken-validateteamemail.pt 2009-07-17 17:59:07 +0000
4632+++ lib/canonical/launchpad/templates/logintoken-validateteamemail.pt 2009-09-16 20:51:57 +0000
4633@@ -3,10 +3,7 @@
4634 xmlns:tal="http://xml.zope.org/namespaces/tal"
4635 xmlns:metal="http://xml.zope.org/namespaces/metal"
4636 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4637- xml:lang="en"
4638- lang="en"
4639- dir="ltr"
4640- metal:use-macro="context/@@main_template/master"
4641+ metal:use-macro="view/macro:page/locationless"
4642 i18n:domain="launchpad"
4643 >
4644 <body>
4645
4646=== modified file 'lib/canonical/launchpad/templates/main-template.pt'
4647--- lib/canonical/launchpad/templates/main-template.pt 2009-08-30 13:31:02 +0000
4648+++ lib/canonical/launchpad/templates/main-template.pt 2009-09-15 17:30:59 +0000
4649@@ -98,12 +98,6 @@
4650 <input type="search" id="search-text" name="field.text" />
4651 </form>
4652 <tal:hierarchy replace="structure context/@@+hierarchy" />
4653- <div id="globalheader" xml:lang="en" lang="en" dir="ltr"
4654- tal:condition="site_message">
4655- <div class="sitemessage" tal:content="structure site_message">
4656- This site is running pre-release code.
4657- </div>
4658- </div>
4659 <div
4660 tal:condition="view/macro:pagehas/applicationtabs"
4661 tal:define="facetmenu view/menu:facet"
4662@@ -239,6 +233,10 @@
4663 <a tal:condition="request/lp:person" href="/feedback"
4664 >Contact us</a> | <a href="https://help.launchpad.net/">Get help with Launchpad</a>
4665 </div>
4666+
4667+ <metal:site-message
4668+ use-macro="context/@@+base-layout-macros/site-message"/>
4669+
4670 <div id="lp-arcana">
4671 &copy;&nbsp;2004-2009&nbsp;<a
4672 href="http://canonical.com/">Canonical&nbsp;Ltd.</a>
4673
4674=== modified file 'lib/canonical/launchpad/templates/structural-subscriptions-manage.pt'
4675--- lib/canonical/launchpad/templates/structural-subscriptions-manage.pt 2009-07-17 17:59:07 +0000
4676+++ lib/canonical/launchpad/templates/structural-subscriptions-manage.pt 2009-09-17 17:12:58 +0000
4677@@ -3,41 +3,28 @@
4678 xmlns:tal="http://xml.zope.org/namespaces/tal"
4679 xmlns:metal="http://xml.zope.org/namespaces/metal"
4680 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
4681- xml:lang="en"
4682- lang="en"
4683- dir="ltr"
4684- metal:use-macro="context/@@main_template/master"
4685+ metal:use-macro="view/macro:page/main_side"
4686 i18n:domain="launchpad"
4687 >
4688 <body>
4689-
4690-<metal:leftportlets fill-slot="portlets_one">
4691- <div tal:replace="structure context/@@+portlet-malone-bugmail-filtering-faq"/>
4692-</metal:leftportlets>
4693-
4694-<metal:rightportlets fill-slot="portlets_two">
4695- <div tal:condition="view/show_details_portlet"
4696- tal:replace="structure context/@@+portlet-details" />
4697- <div tal:replace="structure context/@@+portlet-structural-subscribers" />
4698-</metal:rightportlets>
4699-
4700-<metal:heading fill-slot="pageheading">
4701- <h1>Change bug subscriptions</h1>
4702-</metal:heading>
4703-
4704-<div metal:fill-slot="main">
4705-
4706- <p>
4707- You can choose to receive an e-mail every time someone reports or
4708- changes a public bug associated with
4709- <span tal:replace="context/title">this item</span>.
4710- </p>
4711- <p>
4712- <strong>Important:</strong> subscribing here may mean you receive a
4713- great deal of e-mail. You can return here to unsubscribe at any
4714- time.
4715- </p>
4716- <div metal:use-macro="context/@@launchpad_form/form" />
4717-</div>
4718+ <div metal:fill-slot="main">
4719+ <p>
4720+ You can choose to receive an e-mail every time someone reports or
4721+ changes a public bug associated with
4722+ <span tal:replace="context/title">this item</span>.
4723+ </p>
4724+ <p>
4725+ <strong>Important:</strong> subscribing here may mean you receive a
4726+ great deal of e-mail. You can return here to unsubscribe at any
4727+ time.
4728+ </p>
4729+ <div metal:use-macro="context/@@launchpad_form/form" />
4730+ </div>
4731+ <div metal:fill-slot="side">
4732+ <div tal:replace="structure context/@@+portlet-malone-bugmail-filtering-faq"/>
4733+ <div tal:condition="view/show_details_portlet"
4734+ tal:replace="structure context/@@+portlet-details" />
4735+ <div tal:replace="structure context/@@+portlet-structural-subscribers" />
4736+ </div>
4737 </body>
4738 </html>
4739
4740=== modified file 'lib/canonical/launchpad/testing/fakepackager.py'
4741--- lib/canonical/launchpad/testing/fakepackager.py 2009-06-25 05:30:52 +0000
4742+++ lib/canonical/launchpad/testing/fakepackager.py 2009-09-14 04:07:18 +0000
4743@@ -354,7 +354,7 @@
4744 'Selected upstream directory does not exist: %s' % (
4745 os.path.basename(self.upstream_directory)))
4746
4747- debuild_options = ['-S']
4748+ debuild_options = ['--no-conf', '-S']
4749
4750 if not signed:
4751 debuild_options.extend(['-uc', '-us'])
4752
4753=== modified file 'lib/canonical/launchpad/testing/pages.py'
4754--- lib/canonical/launchpad/testing/pages.py 2009-08-27 19:55:58 +0000
4755+++ lib/canonical/launchpad/testing/pages.py 2009-09-01 09:54:54 +0000
4756@@ -258,7 +258,8 @@
4757 for col_num, item in enumerate(row.findAll('td')):
4758 if columns is None or col_num in columns:
4759 row_content.append(extract_text(item))
4760- print sep.join(row_content)
4761+ if len(row_content) > 0:
4762+ print sep.join(row_content)
4763
4764 def print_radio_button_field(content, name):
4765 """Find the input called field.name, and print a friendly representation.
4766
4767=== modified file 'lib/canonical/launchpad/utilities/searchservice.py'
4768--- lib/canonical/launchpad/utilities/searchservice.py 2009-06-25 05:30:52 +0000
4769+++ lib/canonical/launchpad/utilities/searchservice.py 2009-09-04 10:43:39 +0000
4770@@ -13,7 +13,10 @@
4771 'PageMatches',
4772 ]
4773
4774-import cElementTree as ET
4775+try:
4776+ import xml.etree.cElementTree as ET
4777+except ImportError:
4778+ import cElementTree as ET
4779 import urllib
4780 from urlparse import urlunparse
4781
4782
4783=== modified file 'lib/canonical/launchpad/utilities/unicode_csv.py'
4784--- lib/canonical/launchpad/utilities/unicode_csv.py 2009-06-25 05:30:52 +0000
4785+++ lib/canonical/launchpad/utilities/unicode_csv.py 2009-09-04 11:42:13 +0000
4786@@ -41,6 +41,17 @@
4787 class UnicodeCSVReader:
4788 """A CSV reader that reads encoded files and yields unicode."""
4789
4790+ class DelegateLineNumAccessDescriptor:
4791+ """The Python 2.5 DictReader expects its reader to support access to a
4792+ line_num attribute, therefore to keep UnicodeCSVReader capable of being
4793+ used within a DictReader we provide a line_num attribute which
4794+ delegates to the real reader."""
4795+
4796+ def __get__(self, obj, type):
4797+ return obj.reader.line_num
4798+
4799+ line_num = DelegateLineNumAccessDescriptor()
4800+
4801 def __init__(self, file_, dialect=csv.excel, encoding="utf-8", **kwds):
4802 file_ = UTF8Recoder(file_, encoding)
4803 self.reader = csv.reader(file_, dialect=dialect, **kwds)
4804
4805=== modified file 'lib/canonical/launchpad/versioninfo.py'
4806--- lib/canonical/launchpad/versioninfo.py 2009-06-25 05:30:52 +0000
4807+++ lib/canonical/launchpad/versioninfo.py 2009-09-02 19:11:01 +0000
4808@@ -1,7 +1,7 @@
4809 # Copyright 2009 Canonical Ltd. This software is licensed under the
4810 # GNU Affero General Public License version 3 (see the file LICENSE).
4811
4812-"""Give access to bzr version info, if available.
4813+"""Give access to bzr and other version info, if available.
4814
4815 The bzr version info file is expected to be in the Launchpad root in the
4816 file bzr-version-info.py.
4817@@ -16,19 +16,28 @@
4818 If the bzr-version-info.py file does not exist, then revno, date and
4819 branch_nick will all be None.
4820
4821-If that file exists, and contains valid python, revno, date and branch_nick
4822+If that file exists, and contains valid Python, revno, date and branch_nick
4823 will have appropriate values from version_info.
4824
4825-If that file exists, and contains invalid python, there will be an error when
4826+If that file exists, and contains invalid Python, there will be an error when
4827 this module is loaded. This module is imported into
4828 canonical/launchpad/__init__.py so that such errors are caught at start-up.
4829
4830+This module also reads version.txt at the top of the tree (i.e. a sibling of
4831+bzr-version-info.py), which contains the Launchpad release number. If that
4832+file does not exist, we make something up.
4833 """
4834
4835+__all__ = [
4836+ 'branch_nick',
4837+ 'date',
4838+ 'revno',
4839+ 'versioninfo',
4840+ ]
4841+
4842+
4843 import imp
4844
4845-__all__ = ['versioninfo', 'revno', 'date', 'branch_nick']
4846-
4847
4848 def read_version_info():
4849 try:
4850@@ -52,3 +61,14 @@
4851 date = versioninfo.get('date')
4852 branch_nick = versioninfo.get('branch_nick')
4853
4854+
4855+try:
4856+ version_file = open('version.txt')
4857+except IOError:
4858+ release = 'x.y.z'
4859+else:
4860+ try:
4861+ version_data = version_file.read()
4862+ release = version_data.strip()
4863+ finally:
4864+ version_file.close()
4865
4866=== modified file 'lib/canonical/launchpad/webapp/breadcrumb.py'
4867--- lib/canonical/launchpad/webapp/breadcrumb.py 2009-08-25 12:35:23 +0000
4868+++ lib/canonical/launchpad/webapp/breadcrumb.py 2009-09-18 04:06:03 +0000
4869@@ -7,6 +7,9 @@
4870
4871 __all__ = [
4872 'Breadcrumb',
4873+ 'DisplaynameBreadcrumb',
4874+ 'NameBreadcrumb',
4875+ 'TitleBreadcrumb',
4876 ]
4877
4878
4879@@ -27,6 +30,7 @@
4880 implements(IBreadcrumb)
4881
4882 text = None
4883+ _url = None
4884
4885 def __init__(self, context):
4886 self.context = context
4887@@ -46,20 +50,35 @@
4888
4889 @property
4890 def url(self):
4891- return canonical_url(self.context, rootsite=self.rootsite)
4892-
4893- @property
4894- def icon(self):
4895- """See `IBreadcrumb`."""
4896- # Get the <img> tag from the path adapter.
4897- return queryAdapter(
4898- self.context, IPathAdapter, name='image').icon()
4899+ if self._url is None:
4900+ return canonical_url(self.context, rootsite=self.rootsite)
4901+ else:
4902+ return self._url
4903
4904 def __repr__(self):
4905- if self.icon is not None:
4906- icon_repr = " icon='%s'" % self.icon
4907- else:
4908- icon_repr = ""
4909-
4910- return "<%s url='%s' text='%s'%s>" % (
4911- self.__class__.__name__, self.url, self.text, icon_repr)
4912+ return "<%s url='%s' text='%s'>" % (
4913+ self.__class__.__name__, self.url, self.text)
4914+
4915+
4916+class NameBreadcrumb(Breadcrumb):
4917+ """An `IBreadcrumb` that uses the context's name as its text."""
4918+
4919+ @property
4920+ def text(self):
4921+ return self.context.name
4922+
4923+
4924+class DisplaynameBreadcrumb(Breadcrumb):
4925+ """An `IBreadcrumb` that uses the context's displayname as its text."""
4926+
4927+ @property
4928+ def text(self):
4929+ return self.context.displayname
4930+
4931+
4932+class TitleBreadcrumb(Breadcrumb):
4933+ """An `IBreadcrumb` that uses the context's title as its text."""
4934+
4935+ @property
4936+ def text(self):
4937+ return self.context.title
4938
4939=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
4940--- lib/canonical/launchpad/webapp/configure.zcml 2009-08-27 09:00:17 +0000
4941+++ lib/canonical/launchpad/webapp/configure.zcml 2009-09-11 15:26:11 +0000
4942@@ -365,6 +365,13 @@
4943 />
4944
4945 <adapter
4946+ for="lp.registry.interfaces.distroseries.IDistroSeries"
4947+ provides="zope.traversing.interfaces.IPathAdapter"
4948+ factory="canonical.launchpad.webapp.tales.DistroSeriesFormatterAPI"
4949+ name="fmt"
4950+ />
4951+
4952+ <adapter
4953 for="canonical.launchpad.interfaces.IBug"
4954 provides="zope.traversing.interfaces.IPathAdapter"
4955 factory="canonical.launchpad.webapp.tales.BugFormatterAPI"
4956@@ -507,6 +514,24 @@
4957 name="fmt"
4958 />
4959 <adapter
4960+ for="lp.translations.interfaces.translationgroup.ITranslationGroup"
4961+ provides="zope.traversing.interfaces.IPathAdapter"
4962+ factory="canonical.launchpad.webapp.tales.TranslationGroupFormatterAPI"
4963+ name="fmt"
4964+ />
4965+ <adapter
4966+ for="lp.services.worlddata.interfaces.language.ILanguage"
4967+ provides="zope.traversing.interfaces.IPathAdapter"
4968+ factory="canonical.launchpad.webapp.tales.LanguageFormatterAPI"
4969+ name="fmt"
4970+ />
4971+ <adapter
4972+ for="lp.translations.interfaces.pofile.IPOFile"
4973+ provides="zope.traversing.interfaces.IPathAdapter"
4974+ factory="canonical.launchpad.webapp.tales.POFileFormatterAPI"
4975+ name="fmt"
4976+ />
4977+ <adapter
4978 for="*"
4979 provides="zope.traversing.interfaces.IPathAdapter"
4980 factory="canonical.launchpad.webapp.tales.PermissionRequiredQuery"
4981
4982=== modified file 'lib/canonical/launchpad/webapp/error.py'
4983--- lib/canonical/launchpad/webapp/error.py 2009-07-17 18:46:25 +0000
4984+++ lib/canonical/launchpad/webapp/error.py 2009-09-08 22:42:42 +0000
4985@@ -2,6 +2,16 @@
4986 # GNU Affero General Public License version 3 (see the file LICENSE).
4987
4988 __metaclass__ = type
4989+__all__ = [
4990+ 'InvalidBatchSizeView',
4991+ 'NotFoundView',
4992+ 'ProtocolErrorView',
4993+ 'ReadOnlyErrorView',
4994+ 'RequestExpiredView',
4995+ 'SystemErrorView',
4996+ 'TranslationUnavailableView',
4997+ ]
4998+
4999
5000 import sys
The diff has been truncated for viewing.