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
=== modified file '.bzrignore'
--- .bzrignore 2009-08-05 23:08:37 +0000
+++ .bzrignore 2009-09-10 06:11:21 +0000
@@ -50,3 +50,4 @@
50./_pythonpath.py50./_pythonpath.py
51./production-configs51./production-configs
52bzr.dev52bzr.dev
53_trial_temp
5354
=== modified file 'Makefile'
--- Makefile 2009-08-21 17:50:58 +0000
+++ Makefile 2009-09-17 12:56:07 +0000
@@ -90,6 +90,15 @@
90 @echo90 @echo
91 @echo "Running the JavaScript integration test suite"91 @echo "Running the JavaScript integration test suite"
92 @echo92 @echo
93 bin/test $(VERBOSITY) --layer=BugsWindmillLayer
94 bin/test $(VERBOSITY) --layer=CodeWindmillLayer
95
96jscheck_functest: build
97 # Run the old functest Windmill integration tests. The test runner
98 # takes care of setting up the test environment.
99 @echo
100 @echo "Running Windmill funtest integration test suite"
101 @echo
93 bin/jstest102 bin/jstest
94103
95check_mailman: build104check_mailman: build
96105
=== modified file 'buildmailman.py'
--- buildmailman.py 2009-07-17 00:26:05 +0000
+++ buildmailman.py 2009-09-11 02:17:29 +0000
@@ -41,6 +41,14 @@
41 else:41 else:
42 return 042 return 0
4343
44 # sys.path_importer_cache is a mapping of elements of sys.path to importer
45 # objects used to handle them. In Python2.5+ when an element of sys.path is
46 # found to not exist on disk, a NullImporter is created and cached - this
47 # causes Python to never bother re-inspecting the disk for that path
48 # element. We must clear that cache element so that our second attempt to
49 # import MailMan after building it will actually check the disk.
50 del sys.path_importer_cache[mailman_path]
51
44 # Make sure the target directories exist and have the correct52 # Make sure the target directories exist and have the correct
45 # permissions, otherwise configure will complain.53 # permissions, otherwise configure will complain.
46 user, group = as_username_groupname(config.mailman.build_user_group)54 user, group = as_username_groupname(config.mailman.build_user_group)
4755
=== modified file 'buildout-templates/bin/test.in'
--- buildout-templates/bin/test.in 2009-08-10 22:08:05 +0000
+++ buildout-templates/bin/test.in 2009-09-17 17:42:25 +0000
@@ -134,14 +134,13 @@
134from zope.testing import testrunner134from zope.testing import testrunner
135from zope.testing.testrunner import options135from zope.testing.testrunner import options
136136
137defaults = [137defaults = {
138 # Find tests in the tests and ftests directories138 # Find tests in the tests and ftests directories
139 '--tests-pattern=^f?tests$',139 'tests_pattern': '^f?tests$',
140 '--test-path=${buildout:directory}/lib',140 'test_path': ['${buildout:directory}/lib'],
141 '--package=canonical',141 'package': ['canonical', 'lp', 'devscripts'],
142 '--package=lp',142 'layer': ['!(MailmanLayer|WindmillLayer)'],
143 '--layer=!MailmanLayer',143 }
144 ]
145144
146# Monkey-patch os.listdir to randomise the results145# Monkey-patch os.listdir to randomise the results
147original_listdir = os.listdir146original_listdir = os.listdir
@@ -202,7 +201,22 @@
202 options.parser.add_option(201 options.parser.add_option(
203 '--subunit', action='callback', callback=use_subunit)202 '--subunit', action='callback', callback=use_subunit)
204203
205 local_options = options.get_options(args=args, defaults=defaults)204 # tests_pattern is a regexp, so the parsed value is hard to compare
205 # with the default value in the loop below.
206 options.parser.defaults['tests_pattern'] = defaults['tests_pattern']
207 local_options = options.get_options(args=args)
208 # Set our default options, if the options aren't specified.
209 for name, value in defaults.items():
210 parsed_option = getattr(local_options, name)
211 if ((parsed_option == []) or
212 (parsed_option == options.parser.defaults.get(name))):
213 # The option probably wasn't specified on the command line,
214 # let's replace it with our default value. It could be that
215 # the real default (as specified in
216 # zope.testing.testrunner.options) was specified, and we
217 # shouldn't replace it with our default, but it's such and
218 # edge case, so we don't have to care about it.
219 options.parser.defaults[name] = value
206220
207 # Turn on Layer profiling if requested.221 # Turn on Layer profiling if requested.
208 from canonical.testing import profiled222 from canonical.testing import profiled
@@ -216,7 +230,7 @@
216 try:230 try:
217 there = os.getcwd()231 there = os.getcwd()
218 os.chdir('${buildout:directory}')232 os.chdir('${buildout:directory}')
219 result = testrunner.run(defaults)233 result = testrunner.run([])
220 finally:234 finally:
221 os.chdir(there)235 os.chdir(there)
222 # Cribbed from sourcecode/zope/test.py - avoid spurious error during exit.236 # Cribbed from sourcecode/zope/test.py - avoid spurious error during exit.
223237
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2009-08-19 12:28:32 +0000
+++ configs/development/launchpad-lazr.conf 2009-09-14 19:32:10 +0000
@@ -145,6 +145,7 @@
145restricted_upload_port: 58095145restricted_upload_port: 58095
146restricted_download_port: 58085146restricted_download_port: 58085
147restricted_download_url: http://launchpad.dev:58085/147restricted_download_url: http://launchpad.dev:58085/
148use_https = False
148149
149[librarian_server]150[librarian_server]
150root: /var/tmp/fatsam151root: /var/tmp/fatsam
151152
=== modified file 'configs/development/launchpad.conf'
--- configs/development/launchpad.conf 2009-06-12 16:36:02 +0000
+++ configs/development/launchpad.conf 2009-09-11 18:14:50 +0000
@@ -66,7 +66,7 @@
66</eventlog>66</eventlog>
6767
68<logger>68<logger>
69 name zc.zservertracelog69 name zc.tracelog
70 propagate false70 propagate false
7171
72 <logfile>72 <logfile>
7373
=== modified file 'configs/test-playground/launchpad.conf'
--- configs/test-playground/launchpad.conf 2008-11-10 16:12:10 +0000
+++ configs/test-playground/launchpad.conf 2009-09-11 18:14:50 +0000
@@ -66,7 +66,7 @@
66</eventlog>66</eventlog>
6767
68<logger>68<logger>
69 name zc.zservertracelog69 name zc.tracelog
70 propagate false70 propagate false
7171
72 <logfile>72 <logfile>
7373
=== modified file 'configs/testrunner-appserver/launchpad.conf'
--- configs/testrunner-appserver/launchpad.conf 2009-05-12 21:22:02 +0000
+++ configs/testrunner-appserver/launchpad.conf 2009-09-11 18:14:50 +0000
@@ -39,7 +39,7 @@
39</eventlog>39</eventlog>
4040
41<logger>41<logger>
42 name zc.zservertracelog42 name zc.tracelog
43 propagate false43 propagate false
4444
45 <logfile>45 <logfile>
4646
=== modified file 'configs/testrunner/launchpad-lazr.conf'
--- configs/testrunner/launchpad-lazr.conf 2009-08-15 03:43:17 +0000
+++ configs/testrunner/launchpad-lazr.conf 2009-08-28 21:02:50 +0000
@@ -159,6 +159,9 @@
159oops_prefix: TMPCJ159oops_prefix: TMPCJ
160error_dir: /var/tmp/codehosting.test160error_dir: /var/tmp/codehosting.test
161161
162[update_preview_diffs]
163oops_prefix: TUPD
164error_dir: /var/tmp/codehosting.test
162165
163[personalpackagearchive]166[personalpackagearchive]
164root: /var/tmp/ppa.test/167root: /var/tmp/ppa.test/
165168
=== modified file 'cronscripts/create_merge_proposals.py'
--- cronscripts/create_merge_proposals.py 2009-06-24 20:52:01 +0000
+++ cronscripts/create_merge_proposals.py 2009-09-03 19:46:42 +0000
@@ -26,7 +26,7 @@
26 def main(self):26 def main(self):
27 globalErrorUtility.configure('create_merge_proposals')27 globalErrorUtility.configure('create_merge_proposals')
28 job_source = getUtility(ICreateMergeProposalJobSource)28 job_source = getUtility(ICreateMergeProposalJobSource)
29 runner = JobRunner.fromReady(job_source)29 runner = JobRunner.fromReady(job_source, self.logger)
30 runner.runAll()30 runner.runAll()
31 self.logger.info(31 self.logger.info(
32 'Ran %d CreateMergeProposalJobs.' % len(runner.completed_jobs))32 'Ran %d CreateMergeProposalJobs.' % len(runner.completed_jobs))
3333
=== modified file 'cronscripts/mpcreationjobs.py'
--- cronscripts/mpcreationjobs.py 2009-06-24 20:52:01 +0000
+++ cronscripts/mpcreationjobs.py 2009-09-03 19:04:28 +0000
@@ -31,7 +31,7 @@
31 def main(self):31 def main(self):
32 globalErrorUtility.configure('mpcreationjobs')32 globalErrorUtility.configure('mpcreationjobs')
33 job_source = getUtility(IMergeProposalCreatedJobSource)33 job_source = getUtility(IMergeProposalCreatedJobSource)
34 runner = JobRunner.fromReady(job_source)34 runner = JobRunner.fromReady(job_source, self.logger)
35 server = get_scanner_server()35 server = get_scanner_server()
36 server.setUp()36 server.setUp()
37 try:37 try:
3838
=== modified file 'cronscripts/parse-librarian-apache-access-logs.py'
--- cronscripts/parse-librarian-apache-access-logs.py 2009-08-31 15:01:54 +0000
+++ cronscripts/parse-librarian-apache-access-logs.py 2009-09-11 12:11:04 +0000
@@ -16,8 +16,6 @@
1616
17__metaclass__ = type17__metaclass__ = type
1818
19import os
20
21# pylint: disable-msg=W040319# pylint: disable-msg=W0403
22import _pythonpath20import _pythonpath
2321
@@ -26,55 +24,36 @@
26from storm.sqlobject import SQLObjectNotFound24from storm.sqlobject import SQLObjectNotFound
2725
28from canonical.config import config26from canonical.config import config
29from lp.services.worlddata.interfaces.country import ICountrySet
30from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet27from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
31from lp.services.scripts.base import LaunchpadCronScript
32from lp.services.apachelogparser.base import (
33 create_or_update_parsedlog_entry, get_files_to_parse, parse_file)
34from canonical.launchpad.scripts.librarian_apache_log_parser import (28from canonical.launchpad.scripts.librarian_apache_log_parser import (
35 DBUSER, get_library_file_id)29 DBUSER, get_library_file_id)
36from canonical.launchpad.webapp.interfaces import NotFoundError30from lp.services.apachelogparser.script import ParseApacheLogs
3731
3832
39class ParseLibrarianApacheLogs(LaunchpadCronScript):33class ParseLibrarianApacheLogs(ParseApacheLogs):
4034 """An Apache log parser for LibraryFileAlias downloads."""
41 def main(self):35
42 root = config.librarianlogparser.logs_root36 def setUpUtilities(self):
43 files_to_parse = get_files_to_parse(root, os.listdir(root))37 """See `ParseApacheLogs`."""
4438 self.libraryfilealias_set = getUtility(ILibraryFileAliasSet)
45 libraryfilealias_set = getUtility(ILibraryFileAliasSet)39
46 country_set = getUtility(ICountrySet)40 @property
47 for fd, position in files_to_parse.items():41 def root(self):
48 downloads, parsed_bytes = parse_file(42 """See `ParseApacheLogs`."""
49 fd, position, self.logger, get_library_file_id)43 return config.librarianlogparser.logs_root
50 # Use a while loop here because we want to pop items from the dict44
51 # in order to free some memory as we go along. This is a good45 def getDownloadKey(self, path):
52 # thing here because the downloads dict may get really huge.46 """See `ParseApacheLogs`."""
53 while downloads:47 return get_library_file_id(path)
54 file_id, daily_downloads = downloads.popitem()48
55 try:49 def getDownloadCountUpdater(self, file_id):
56 lfa = libraryfilealias_set[file_id]50 """See `ParseApacheLogs`."""
57 except SQLObjectNotFound:51 try:
58 # This file has been deleted from the librarian, so don't52 return self.libraryfilealias_set[file_id].updateDownloadCount
59 # try to store download counters for it.53 except SQLObjectNotFound:
60 continue54 # This file has been deleted from the librarian, so don't
61 for day, country_downloads in daily_downloads.items():55 # try to store download counters for it.
62 for country_code, count in country_downloads.items():56 return None
63 try:
64 country = country_set[country_code]
65 except NotFoundError:
66 # We don't know the country for the IP address
67 # where this request originated.
68 country = None
69 lfa.updateDownloadCount(day, country, count)
70 fd.seek(0)
71 first_line = fd.readline()
72 fd.close()
73 create_or_update_parsedlog_entry(first_line, parsed_bytes)
74 self.txn.commit()
75 self.logger.info('Finished parsing %s' % fd)
76
77 self.logger.info('Done parsing apache log files for librarian')
7857
7958
80if __name__ == '__main__':59if __name__ == '__main__':
8160
=== modified file 'cronscripts/reclaimbranchspace.py'
--- cronscripts/reclaimbranchspace.py 2009-07-17 02:25:09 +0000
+++ cronscripts/reclaimbranchspace.py 2009-09-03 20:06:45 +0000
@@ -26,7 +26,7 @@
26 def main(self):26 def main(self):
27 globalErrorUtility.configure('reclaimbranchspace')27 globalErrorUtility.configure('reclaimbranchspace')
28 job_source = getUtility(IReclaimBranchSpaceJobSource)28 job_source = getUtility(IReclaimBranchSpaceJobSource)
29 runner = JobRunner.fromReady(job_source)29 runner = JobRunner.fromReady(job_source, self.logger)
30 runner.runAll()30 runner.runAll()
31 self.logger.info(31 self.logger.info(
32 'Reclaimed space for %s branches.', len(runner.completed_jobs))32 'Reclaimed space for %s branches.', len(runner.completed_jobs))
3333
=== modified file 'cronscripts/rosetta-branches.py'
--- cronscripts/rosetta-branches.py 2009-06-24 20:52:01 +0000
+++ cronscripts/rosetta-branches.py 2009-09-03 20:29:25 +0000
@@ -29,7 +29,8 @@
2929
30 def main(self):30 def main(self):
31 globalErrorUtility.configure('rosettabranches')31 globalErrorUtility.configure('rosettabranches')
32 runner = JobRunner.fromReady(getUtility(IRosettaUploadJobSource))32 runner = JobRunner.fromReady(
33 getUtility(IRosettaUploadJobSource), self.logger)
33 server = get_scanner_server()34 server = get_scanner_server()
34 server.setUp()35 server.setUp()
35 try:36 try:
3637
=== modified file 'cronscripts/sendbranchmail.py'
--- cronscripts/sendbranchmail.py 2009-06-24 20:52:01 +0000
+++ cronscripts/sendbranchmail.py 2009-09-03 19:25:57 +0000
@@ -31,7 +31,7 @@
31 globalErrorUtility.configure('sendbranchmail')31 globalErrorUtility.configure('sendbranchmail')
32 jobs = list(getUtility(IRevisionMailJobSource).iterReady())32 jobs = list(getUtility(IRevisionMailJobSource).iterReady())
33 jobs.extend(getUtility(IRevisionsAddedJobSource).iterReady())33 jobs.extend(getUtility(IRevisionsAddedJobSource).iterReady())
34 runner = JobRunner(jobs)34 runner = JobRunner(jobs, self.logger)
35 server = get_scanner_server()35 server = get_scanner_server()
36 server.setUp()36 server.setUp()
37 try:37 try:
3838
=== added file 'cronscripts/update_preview_diffs.py'
--- cronscripts/update_preview_diffs.py 1970-01-01 00:00:00 +0000
+++ cronscripts/update_preview_diffs.py 2009-09-01 19:00:46 +0000
@@ -0,0 +1,34 @@
1#!/usr/bin/python2.4
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6# pylint: disable-msg=W0403
7
8"""Update or create previews diffs for branch merge proposals."""
9
10__metaclass__ = type
11
12import _pythonpath
13
14from lp.codehosting.vfs import get_scanner_server
15from lp.services.job.runner import JobCronScript
16from lp.code.interfaces.branchmergeproposal import (
17 IUpdatePreviewDiffJobSource,)
18
19
20class RunUpdatePreviewDiffJobs(JobCronScript):
21 """Run UpdatePreviewDiff jobs."""
22
23 config_name = 'update_preview_diffs'
24 source_interface = IUpdatePreviewDiffJobSource
25
26 def setUp(self):
27 server = get_scanner_server()
28 server.setUp()
29 return [server.tearDown]
30
31
32if __name__ == '__main__':
33 script = RunUpdatePreviewDiffJobs()
34 script.lock_and_run()
035
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2009-08-29 19:37:54 +0000
+++ database/schema/security.cfg 2009-09-02 19:06:19 +0000
@@ -579,6 +579,7 @@
579public.branch = SELECT, UPDATE579public.branch = SELECT, UPDATE
580public.branchjob = SELECT, INSERT, UPDATE, DELETE580public.branchjob = SELECT, INSERT, UPDATE, DELETE
581public.branchmergeproposal = SELECT, UPDATE581public.branchmergeproposal = SELECT, UPDATE
582public.branchmergeproposaljob = SELECT, INSERT
582public.branchrevision = SELECT, INSERT, UPDATE, DELETE583public.branchrevision = SELECT, INSERT, UPDATE, DELETE
583public.branchsubscription = SELECT584public.branchsubscription = SELECT
584public.branchvisibilitypolicy = SELECT585public.branchvisibilitypolicy = SELECT
@@ -1590,6 +1591,18 @@
1590public.teamparticipation = SELECT1591public.teamparticipation = SELECT
1591public.validpersoncache = SELECT1592public.validpersoncache = SELECT
15921593
1594[update-preview-diffs]
1595type=user
1596groups=script
1597public.branch = SELECT
1598public.branchmergeproposal = SELECT, UPDATE
1599public.branchmergeproposaljob = SELECT
1600public.diff = SELECT, INSERT
1601public.job = SELECT, UPDATE
1602public.libraryfilealias = SELECT, INSERT
1603public.libraryfilecontent = SELECT, INSERT
1604public.previewdiff = SELECT, INSERT
1605
1593[send-branch-mail]1606[send-branch-mail]
1594type=user1607type=user
1595groups=script1608groups=script
15961609
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2009-08-29 19:37:54 +0000
+++ lib/canonical/config/schema-lazr.conf 2009-09-14 19:32:10 +0000
@@ -1144,6 +1144,8 @@
1144# datatype: urlbase1144# datatype: urlbase
1145restricted_download_url: http://restricted-librarian.launchpad.net/1145restricted_download_url: http://restricted-librarian.launchpad.net/
11461146
1147use_https = True
1148
1147[librarian_gc]1149[librarian_gc]
1148# The database user which will be used by this process.1150# The database user which will be used by this process.
1149# datatype: string1151# datatype: string
@@ -1346,6 +1348,14 @@
1346comments_list_max_length: 1001348comments_list_max_length: 100
1347comments_list_truncate_to: 801349comments_list_truncate_to: 80
13481350
1351# Should +filebug be disabled for Ubuntu ?
1352ubuntu_disable_filebug: false
1353
1354# Redirect to this URL when users try to file a bug on Ubuntu without
1355# using apport.
1356ubuntu_bug_filing_url: https://help.ubuntu.com/community/ReportingBugs
1357
1358
1349[mpcreationjobs]1359[mpcreationjobs]
1350# The database user which will be used by this process.1360# The database user which will be used by this process.
1351# datatype: string1361# datatype: string
@@ -1362,6 +1372,18 @@
1362# See [error_reports].1372# See [error_reports].
1363copy_to_zlog: false1373copy_to_zlog: false
13641374
1375[update_preview_diffs]
1376dbuser: update-preview-diffs
1377
1378# See [error_reports].
1379error_dir: none
1380
1381# See [error_reports].
1382oops_prefix: none
1383
1384# See [error_reports].
1385copy_to_zlog: false
1386
1365[person_notification]1387[person_notification]
1366# User for person notification db access1388# User for person notification db access
1367# datatype: string1389# datatype: string
@@ -1409,6 +1431,10 @@
1409storm_cache: generational1431storm_cache: generational
1410storm_cache_size: 5001432storm_cache_size: 500
14111433
1434# Statement timeout (in seconds), limited to super-fast-imports query.
1435# Set to 'timeout' to make it timeout every time (for tests).
1436statement_timeout: 300
1437
1412[processmail]1438[processmail]
1413# The database user which will be used by this process.1439# The database user which will be used by this process.
1414# datatype: string1440# datatype: string
14151441
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py 2009-08-28 06:38:41 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2009-09-16 19:56:45 +0000
@@ -9,12 +9,11 @@
9 'ApplicationButtons',9 'ApplicationButtons',
10 'BrowserWindowDimensions',10 'BrowserWindowDimensions',
11 'DoesNotExistView',11 'DoesNotExistView',
12 'get_launchpad_views',
13 'Hierarchy',12 'Hierarchy',
14 'IcingContribFolder',13 'IcingContribFolder',
15 'IcingFolder',14 'IcingFolder',
15 'LaunchpadImageFolder',
16 'LaunchpadRootNavigation',16 'LaunchpadRootNavigation',
17 'LaunchpadImageFolder',
18 'LinkView',17 'LinkView',
19 'LoginStatus',18 'LoginStatus',
20 'MaintenanceMessage',19 'MaintenanceMessage',
@@ -23,6 +22,7 @@
23 'SoftTimeoutView',22 'SoftTimeoutView',
24 'StructuralHeaderPresentation',23 'StructuralHeaderPresentation',
25 'StructuralObjectPresentation',24 'StructuralObjectPresentation',
25 'get_launchpad_views',
26 ]26 ]
2727
2828
@@ -34,6 +34,7 @@
34import urllib34import urllib
35from datetime import timedelta, datetime35from datetime import timedelta, datetime
3636
37from zope.app import zapi
37from zope.datetime import parseDatetimetz, tzinfo, DateTimeError38from zope.datetime import parseDatetimetz, tzinfo, DateTimeError
38from zope.component import getUtility, queryAdapter39from zope.component import getUtility, queryAdapter
39from zope.interface import implements40from zope.interface import implements
@@ -95,6 +96,7 @@
95 LaunchpadFormView, LaunchpadView, Link, Navigation,96 LaunchpadFormView, LaunchpadView, Link, Navigation,
96 StandardLaunchpadFacets, canonical_name, canonical_url, custom_widget,97 StandardLaunchpadFacets, canonical_name, canonical_url, custom_widget,
97 stepto)98 stepto)
99from canonical.launchpad.webapp.breadcrumb import Breadcrumb
98from canonical.launchpad.webapp.interfaces import (100from canonical.launchpad.webapp.interfaces import (
99 IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu,101 IBreadcrumb, ILaunchBag, ILaunchpadRoot, INavigationMenu,
100 NotFoundError, POSTToNonCanonicalURL)102 NotFoundError, POSTToNonCanonicalURL)
@@ -233,25 +235,48 @@
233 breadcrumbs.append(breadcrumb)235 breadcrumbs.append(breadcrumb)
234236
235 host = URI(self.request.getURL()).host237 host = URI(self.request.getURL()).host
236 if (len(breadcrumbs) == 0238 mainhost = allvhosts.configs['mainsite'].hostname
237 or host == allvhosts.configs['mainsite'].hostname):239 if len(breadcrumbs) != 0 and host != mainhost:
238 return breadcrumbs240 # We have breadcrumbs and we're not on the mainsite, so we'll
239241 # sneak an extra breadcrumb for the vhost we're on.
240 # If we got this far it means we have breadcrumbs and we're not on the242 vhost = host.split('.')[0]
241 # mainsite, so we'll sneak an extra breadcrumb for the vhost we're on.243
242 vhost = host.split('.')[0]244 # Iterate over the context of our breadcrumbs in reverse order and
243245 # for the first one we find an adapter named after the vhost we're
244 # Iterate over the context of our breadcrumbs in reverse order and for246 # on, generate an extra breadcrumb and insert it in our list.
245 # the first one we find an adapter named after the vhost we're on,247 for idx, breadcrumb in reversed(list(enumerate(breadcrumbs))):
246 # generate an extra breadcrumb and insert it in our list.248 extra_breadcrumb = queryAdapter(
247 for idx, breadcrumb in reversed(list(enumerate(breadcrumbs))):249 breadcrumb.context, IBreadcrumb, name=vhost)
248 extra_breadcrumb = queryAdapter(250 if extra_breadcrumb is not None:
249 breadcrumb.context, IBreadcrumb, name=vhost)251 breadcrumbs.insert(idx + 1, extra_breadcrumb)
250 if extra_breadcrumb is not None:252 break
251 breadcrumbs.insert(idx + 1, extra_breadcrumb)253 if len(breadcrumbs) > 0:
252 break254 page_crumb = self.makeBreadcrumbForRequestedPage()
255 if page_crumb:
256 breadcrumbs.append(page_crumb)
253 return breadcrumbs257 return breadcrumbs
254258
259 def makeBreadcrumbForRequestedPage(self):
260 """Return an `IBreadcrumb` for the requested page.
261
262 The `IBreadcrumb` for the requested page is created using the current
263 URL and the page's name (i.e. the last path segment of the URL).
264
265 If the requested page (as specified in self.request) is the default
266 one for the last traversed object, return None.
267 """
268 url = self.request.getURL()
269 last_segment = URI(url).path.split('/')[-1]
270 default_view_name = zapi.getDefaultViewName(
271 self.request.traversed_objects[-1], self.request)
272 if last_segment.startswith('+') and last_segment != default_view_name:
273 breadcrumb = Breadcrumb(None)
274 breadcrumb._url = url
275 breadcrumb.text = last_segment
276 return breadcrumb
277 else:
278 return None
279
255 @property280 @property
256 def display_breadcrumbs(self):281 def display_breadcrumbs(self):
257 """Return whether the breadcrumbs should be displayed."""282 """Return whether the breadcrumbs should be displayed."""
@@ -259,6 +284,7 @@
259 # to display it as it will simply repeat the context.title.284 # to display it as it will simply repeat the context.title.
260 return len(self.items) > 1285 return len(self.items) > 1
261286
287
262class MaintenanceMessage:288class MaintenanceMessage:
263 """Display a maintenance message if the control file is present and289 """Display a maintenance message if the control file is present and
264 it contains a valid iso format time.290 it contains a valid iso format time.
265291
=== modified file 'lib/canonical/launchpad/browser/logintoken.py'
--- lib/canonical/launchpad/browser/logintoken.py 2009-07-20 15:27:26 +0000
+++ lib/canonical/launchpad/browser/logintoken.py 2009-09-18 01:09:10 +0000
@@ -93,6 +93,8 @@
93 }93 }
94 login_token_pages.update(auth_token_pages)94 login_token_pages.update(auth_token_pages)
95 PAGES = login_token_pages95 PAGES = login_token_pages
96 page_title = 'You have already done this'
97 label = 'Confirmation already concluded'
9698
97 def render(self):99 def render(self):
98 if self.context.date_consumed is None:100 if self.context.date_consumed is None:
@@ -109,6 +111,11 @@
109 expected_token_types = ()111 expected_token_types = ()
110 successfullyProcessed = False112 successfullyProcessed = False
111113
114 @property
115 def page_title(self):
116 """The page title."""
117 return self.label
118
112 def redirectIfInvalidOrConsumedToken(self):119 def redirectIfInvalidOrConsumedToken(self):
113 """If this is a consumed or invalid token redirect to the LoginToken120 """If this is a consumed or invalid token redirect to the LoginToken
114 default view and return True.121 default view and return True.
@@ -354,6 +361,15 @@
354 expected_token_types = (LoginTokenType.VALIDATEGPG,361 expected_token_types = (LoginTokenType.VALIDATEGPG,
355 LoginTokenType.VALIDATESIGNONLYGPG)362 LoginTokenType.VALIDATESIGNONLYGPG)
356363
364 @property
365 def label(self):
366 if self.context.tokentype == LoginTokenType.VALIDATESIGNONLYGPG:
367 return 'Confirm sign-only OpenPGP key'
368 else:
369 assert self.context.tokentype == LoginTokenType.VALIDATEGPG, (
370 'unexpected token type: %r' % self.context.tokentype)
371 return 'Confirm OpenPGP key'
372
357 def initialize(self):373 def initialize(self):
358 if not self.redirectIfInvalidOrConsumedToken():374 if not self.redirectIfInvalidOrConsumedToken():
359 if self.context.tokentype == LoginTokenType.VALIDATESIGNONLYGPG:375 if self.context.tokentype == LoginTokenType.VALIDATESIGNONLYGPG:
@@ -571,6 +587,7 @@
571 schema = Interface587 schema = Interface
572 field_names = []588 field_names = []
573 expected_token_types = (LoginTokenType.VALIDATEEMAIL,)589 expected_token_types = (LoginTokenType.VALIDATEEMAIL,)
590 label = 'Confirm e-mail address'
574591
575 def initialize(self):592 def initialize(self):
576 self.redirectIfInvalidOrConsumedToken()593 self.redirectIfInvalidOrConsumedToken()
@@ -670,6 +687,7 @@
670class ValidateTeamEmailView(ValidateEmailView):687class ValidateTeamEmailView(ValidateEmailView):
671688
672 expected_token_types = (LoginTokenType.VALIDATETEAMEMAIL,)689 expected_token_types = (LoginTokenType.VALIDATETEAMEMAIL,)
690 # The desired label is the same as ValidateEmailView.
673691
674 def markEmailAsValid(self, email):692 def markEmailAsValid(self, email):
675 """See `ValidateEmailView`"""693 """See `ValidateEmailView`"""
@@ -679,6 +697,7 @@
679class MergePeopleView(BaseTokenView, LaunchpadView):697class MergePeopleView(BaseTokenView, LaunchpadView):
680 expected_token_types = (LoginTokenType.ACCOUNTMERGE,)698 expected_token_types = (LoginTokenType.ACCOUNTMERGE,)
681 mergeCompleted = False699 mergeCompleted = False
700 label = 'Merge Launchpad accounts'
682701
683 def initialize(self):702 def initialize(self):
684 self.redirectIfInvalidOrConsumedToken()703 self.redirectIfInvalidOrConsumedToken()
685704
=== modified file 'lib/canonical/launchpad/browser/packaging.py'
--- lib/canonical/launchpad/browser/packaging.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/browser/packaging.py 2009-09-12 06:11:08 +0000
@@ -18,9 +18,15 @@
1818
19class PackagingAddView(LaunchpadFormView):19class PackagingAddView(LaunchpadFormView):
20 schema = IPackaging20 schema = IPackaging
21 label = 'Add distribution packaging record'
22 field_names = ['distroseries', 'sourcepackagename', 'packaging']21 field_names = ['distroseries', 'sourcepackagename', 'packaging']
2322
23 @property
24 def label(self):
25 """See `LaunchpadFormView`."""
26 return 'Packaging of %s in distributions' % self.context.displayname
27
28 page_title = label
29
24 def validate(self, data):30 def validate(self, data):
25 productseries = self.context31 productseries = self.context
26 sourcepackagename = data['sourcepackagename']32 sourcepackagename = data['sourcepackagename']
2733
=== modified file 'lib/canonical/launchpad/browser/structuralsubscription.py'
--- lib/canonical/launchpad/browser/structuralsubscription.py 2009-08-24 01:09:07 +0000
+++ lib/canonical/launchpad/browser/structuralsubscription.py 2009-09-17 17:12:58 +0000
@@ -34,6 +34,16 @@
34 custom_widget('subscriptions_team', LabeledMultiCheckBoxWidget)34 custom_widget('subscriptions_team', LabeledMultiCheckBoxWidget)
35 custom_widget('remove_other_subscriptions', LabeledMultiCheckBoxWidget)35 custom_widget('remove_other_subscriptions', LabeledMultiCheckBoxWidget)
3636
37 override_title_breadcrumbs = True
38
39 @property
40 def page_title(self):
41 return 'Subscribe to Bugs in %s' % self.context.title
42
43 @property
44 def label(self):
45 return self.page_title
46
37 def setUpFields(self):47 def setUpFields(self):
38 """See LaunchpadFormView."""48 """See LaunchpadFormView."""
39 LaunchpadFormView.setUpFields(self)49 LaunchpadFormView.setUpFields(self)
@@ -167,7 +177,7 @@
167 'e-mail each time someone reports or changes one of '177 'e-mail each time someone reports or changes one of '
168 'its public bugs.' % target.displayname)178 'its public bugs.' % target.displayname)
169 elif is_subscribed and not subscribe:179 elif is_subscribed and not subscribe:
170 target.removeBugSubscription(self.user)180 target.removeBugSubscription(self.user, self.user)
171 self.request.response.addNotification(181 self.request.response.addNotification(
172 'You have unsubscribed from "%s". You '182 'You have unsubscribed from "%s". You '
173 'will no longer automatically receive e-mail about '183 'will no longer automatically receive e-mail about '
@@ -197,7 +207,7 @@
197 team.displayname, self.context.displayname))207 team.displayname, self.context.displayname))
198208
199 for team in subscriptions - form_selected_teams:209 for team in subscriptions - form_selected_teams:
200 target.removeBugSubscription(team)210 target.removeBugSubscription(team, self.user)
201 self.request.response.addNotification(211 self.request.response.addNotification(
202 'The %s team will no longer automatically receive '212 'The %s team will no longer automatically receive '
203 'e-mail about changes to public bugs in "%s".' % (213 'e-mail about changes to public bugs in "%s".' % (
@@ -220,7 +230,7 @@
220230
221 subscriptions_to_remove = data.get('remove_other_subscriptions', [])231 subscriptions_to_remove = data.get('remove_other_subscriptions', [])
222 for subscription in subscriptions_to_remove:232 for subscription in subscriptions_to_remove:
223 target.removeBugSubscription(subscription)233 target.removeBugSubscription(subscription, self.user)
224 self.request.response.addNotification(234 self.request.response.addNotification(
225 '%s will no longer automatically receive e-mail about '235 '%s will no longer automatically receive e-mail about '
226 'public bugs in "%s".' % (236 'public bugs in "%s".' % (
227237
=== modified file 'lib/canonical/launchpad/database/librarian.py'
--- lib/canonical/launchpad/database/librarian.py 2009-08-27 01:32:01 +0000
+++ lib/canonical/launchpad/database/librarian.py 2009-09-14 19:32:10 +0000
@@ -98,7 +98,7 @@
9898
99 def getURL(self):99 def getURL(self):
100 """See ILibraryFileAlias.getURL"""100 """See ILibraryFileAlias.getURL"""
101 if config.vhosts.use_https:101 if config.librarian.use_https:
102 return self.https_url102 return self.https_url
103 else:103 else:
104 return self.http_url104 return self.http_url
105105
=== modified file 'lib/canonical/launchpad/database/structuralsubscription.py'
--- lib/canonical/launchpad/database/structuralsubscription.py 2009-07-23 13:44:13 +0000
+++ lib/canonical/launchpad/database/structuralsubscription.py 2009-08-25 12:04:58 +0000
@@ -5,6 +5,7 @@
5__all__ = ['StructuralSubscription',5__all__ = ['StructuralSubscription',
6 'StructuralSubscriptionTargetMixin']6 'StructuralSubscriptionTargetMixin']
77
8from zope.component import getUtility
8from zope.interface import implements9from zope.interface import implements
910
10from sqlobject import ForeignKey11from sqlobject import ForeignKey
@@ -16,9 +17,10 @@
1617
17from canonical.launchpad.interfaces import (18from canonical.launchpad.interfaces import (
18 BlueprintNotificationLevel, BugNotificationLevel, DeleteSubscriptionError,19 BlueprintNotificationLevel, BugNotificationLevel, DeleteSubscriptionError,
19 IDistribution, IDistributionSourcePackage, IDistroSeries, IMilestone,20 IDistribution, IDistributionSourcePackage, IDistroSeries,
20 IProduct, IProductSeries, IProject, IStructuralSubscription,21 ILaunchpadCelebrities, IMilestone, IProduct, IProductSeries, IProject,
21 IStructuralSubscriptionTarget)22 IStructuralSubscription, IStructuralSubscriptionTarget,
23 UserCannotSubscribePerson)
22from lp.registry.interfaces.person import (24from lp.registry.interfaces.person import (
23 validate_public_person, validate_person_not_private_membership)25 validate_public_person, validate_person_not_private_membership)
2426
@@ -129,8 +131,35 @@
129 '%s is not a valid structural subscription target.')131 '%s is not a valid structural subscription target.')
130 return args132 return args
131133
134 def _userCanAlterSubscription(self, subscriber, subscribed_by):
135 """Check if a user can change a subscription for a person."""
136 # A Launchpad administrator or the user can subscribe a user.
137 # A Launchpad or team admin can subscribe a team.
138
139 # Nobody else can, unless the context is a IDistributionSourcePackage,
140 # in which case the drivers or owner can.
141 if IDistributionSourcePackage.providedBy(self):
142 for driver in self.distribution.drivers:
143 if subscribed_by.inTeam(driver):
144 return True
145 if subscribed_by.inTeam(self.distribution.owner):
146 return True
147
148 admins = getUtility(ILaunchpadCelebrities).admin
149 return (subscriber is subscribed_by or
150 subscriber in subscribed_by.getAdministratedTeams() or
151 subscribed_by.inTeam(admins))
152
132 def addSubscription(self, subscriber, subscribed_by):153 def addSubscription(self, subscriber, subscribed_by):
133 """See `IStructuralSubscriptionTarget`."""154 """See `IStructuralSubscriptionTarget`."""
155 if subscriber is None:
156 subscriber = subscribed_by
157
158 if not self._userCanAlterSubscription(subscriber, subscribed_by):
159 raise UserCannotSubscribePerson(
160 '%s does not have permission to subscribe %s.' % (
161 subscribed_by.name, subscriber.name))
162
134 existing_subscription = self.getSubscription(subscriber)163 existing_subscription = self.getSubscription(subscriber)
135164
136 if existing_subscription is not None:165 if existing_subscription is not None:
@@ -151,20 +180,28 @@
151 sub.bug_notification_level = BugNotificationLevel.COMMENTS180 sub.bug_notification_level = BugNotificationLevel.COMMENTS
152 return sub181 return sub
153182
154 def removeBugSubscription(self, person):183 def removeBugSubscription(self, subscriber, unsubscribed_by):
155 """See `IStructuralSubscriptionTarget`."""184 """See `IStructuralSubscriptionTarget`."""
185 if subscriber is None:
186 subscriber = unsubscribed_by
187
188 if not self._userCanAlterSubscription(subscriber, unsubscribed_by):
189 raise UserCannotSubscribePerson(
190 '%s does not have permission to unsubscribe %s.' % (
191 unsubscribed_by.name, subscriber.name))
192
156 subscription_to_remove = None193 subscription_to_remove = None
157 for subscription in self.getSubscriptions(194 for subscription in self.getSubscriptions(
158 min_bug_notification_level=BugNotificationLevel.METADATA):195 min_bug_notification_level=BugNotificationLevel.METADATA):
159 # Only search for bug subscriptions196 # Only search for bug subscriptions
160 if subscription.subscriber == person:197 if subscription.subscriber == subscriber:
161 subscription_to_remove = subscription198 subscription_to_remove = subscription
162 break199 break
163200
164 if subscription_to_remove is None:201 if subscription_to_remove is None:
165 raise DeleteSubscriptionError(202 raise DeleteSubscriptionError(
166 "%s is not subscribed to %s." % (203 "%s is not subscribed to %s." % (
167 person.name, self.displayname))204 subscriber.name, self.displayname))
168 else:205 else:
169 if (subscription_to_remove.blueprint_notification_level >206 if (subscription_to_remove.blueprint_notification_level >
170 BlueprintNotificationLevel.NOTHING):207 BlueprintNotificationLevel.NOTHING):
171208
=== modified file 'lib/canonical/launchpad/doc/canonical_url_examples.txt'
--- lib/canonical/launchpad/doc/canonical_url_examples.txt 2009-08-25 22:27:24 +0000
+++ lib/canonical/launchpad/doc/canonical_url_examples.txt 2009-09-10 10:27:20 +0000
@@ -22,7 +22,7 @@
2222
23 >>> from canonical.launchpad.interfaces import (23 >>> from canonical.launchpad.interfaces import (
24 ... IMaloneApplication, IBazaarApplication,24 ... IMaloneApplication, IBazaarApplication,
25 ... IRosettaApplication, ILaunchpadRoot, IQuestionSet25 ... ILaunchpadRoot, IQuestionSet
26 ... )26 ... )
2727
28The Launchpad homepage.28The Launchpad homepage.
@@ -35,11 +35,6 @@
35 >>> canonical_url(getUtility(IMaloneApplication))35 >>> canonical_url(getUtility(IMaloneApplication))
36 u'http://launchpad.dev/bugs'36 u'http://launchpad.dev/bugs'
3737
38The Rosetta homepage.
39
40 >>> canonical_url(getUtility(IRosettaApplication))
41 u'http://launchpad.dev/translations'
42
43The Bazaar homepage.38The Bazaar homepage.
4439
45 >>> canonical_url(getUtility(IBazaarApplication))40 >>> canonical_url(getUtility(IBazaarApplication))
@@ -56,6 +51,9 @@
56 >>> canonical_url(getUtility(IMailingListSet))51 >>> canonical_url(getUtility(IMailingListSet))
57 u'http://launchpad.dev/+mailinglists'52 u'http://launchpad.dev/+mailinglists'
5853
54Launchpad Translations (Rosetta) canonical_url examples are in
55lib/lp/translations/doc/canonical_url_examples.txt.
56
5957
60== Persons and Teams ==58== Persons and Teams ==
6159
@@ -300,20 +298,20 @@
300An IBugTrackerSet.298An IBugTrackerSet.
301299
302 >>> canonical_url(getUtility(IBugTrackerSet))300 >>> canonical_url(getUtility(IBugTrackerSet))
303 u'http://launchpad.dev/bugs/bugtrackers'301 u'http://bugs.launchpad.dev/bugs/bugtrackers'
304302
305A remote bug tracker.303A remote bug tracker.
306304
307 >>> mozilla_bugtracker = getUtility(IBugTrackerSet)['mozilla.org']305 >>> mozilla_bugtracker = getUtility(IBugTrackerSet)['mozilla.org']
308 >>> canonical_url(mozilla_bugtracker)306 >>> canonical_url(mozilla_bugtracker)
309 u'http://launchpad.dev/bugs/bugtrackers/mozilla.org'307 u'http://bugs.launchpad.dev/bugs/bugtrackers/mozilla.org'
310308
311A bug from a remote bug tracker.309A bug from a remote bug tracker.
312310
313 >>> remote_bug = RemoteBug(mozilla_bugtracker, '42',311 >>> remote_bug = RemoteBug(mozilla_bugtracker, '42',
314 ... mozilla_bugtracker.getBugsWatching('42'))312 ... mozilla_bugtracker.getBugsWatching('42'))
315 >>> canonical_url(remote_bug)313 >>> canonical_url(remote_bug)
316 u'http://launchpad.dev/bugs/bugtrackers/mozilla.org/42'314 u'http://bugs.launchpad.dev/bugs/bugtrackers/mozilla.org/42'
317315
318316
319== Branches ==317== Branches ==
@@ -363,97 +361,6 @@
363 http://code.launchpad.dev/~name12/gnome-terminal/main/+merge/.../comments/...361 http://code.launchpad.dev/~name12/gnome-terminal/main/+merge/.../comments/...
364362
365363
366== POTemplates and so on ==
367
368 >>> from lp.translations.interfaces.potemplate import IPOTemplateSet
369 >>> from lp.translations.interfaces.translationgroup import (
370 ... ITranslationGroupSet)
371
372Most Rosetta pages hang off IPOTemplateSubset objects, of which there are two
373varieties: distribution and upstream.
374
375First, the distribution kind. We'll need the source package name.
376
377 >>> sourcepackagename = sourcepackagenameset['evolution']
378
379And here's our subset.
380
381 >>> potemplateset = getUtility(IPOTemplateSet)
382 >>> potemplatesubset = potemplateset.getSubset(
383 ... distroseries=hoary, sourcepackagename=sourcepackagename)
384
385 >>> canonical_url(potemplatesubset)
386 u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots'
387
388We can get a particular PO template for this source package by its PO template
389name.
390
391 >>> potemplate = potemplatesubset['evolution-2.2']
392 >>> canonical_url(potemplate)
393 u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2'
394
395And we can get a particular PO file for this PO template by its language code.
396
397 >>> pofile = potemplate.getPOFileByLang('es')
398 >>> canonical_url(pofile)
399 u'http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es'
400
401Also, we can get the url to a translation message.
402
403 >>> potmsgset = potemplate.getPOTMsgSetBySequence(1)
404 >>> translationmessage = potmsgset.getCurrentTranslationMessage(
405 ... pofile.potemplate, pofile.language)
406 >>> translationmessage.setPOFile(pofile)
407 >>> print canonical_url(translationmessage)
408 http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/1
409
410Even for a dummy one.
411
412 >>> potmsgset = potemplate.getPOTMsgSetBySequence(20)
413 >>> translationmessage = potmsgset.getCurrentDummyTranslationMessage(
414 ... pofile.potemplate, pofile.language)
415 >>> print canonical_url(translationmessage)
416 http://launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/es/20
417
418Upstream POTemplateSubsets work in much the same way, except they hang off a
419product series. Let's get a product series.
420
421Now we can get an upstream subset and do the same sorts of thing as we did
422with the distro subset.
423
424 >>> potemplatesubset = potemplateset.getSubset(
425 ... productseries=evolution_trunk_series)
426 >>> potemplate = potemplatesubset['evolution-2.2']
427 >>> canonical_url(potemplate)
428 u'http://launchpad.dev/evolution/trunk/+pots/evolution-2.2'
429
430 >>> pofile = potemplate.getPOFileByLang('es')
431 >>> canonical_url(pofile)
432 u'http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es'
433
434Also, we can get the url to a dummy one
435
436 >>> potmsgset = potemplate.getPOTMsgSetBySequence(1)
437 >>> translationmessage = potmsgset.getCurrentTranslationMessage(
438 ... pofile.potemplate, pofile.language)
439 >>> translationmessage.setPOFile(pofile)
440 >>> print canonical_url(translationmessage)
441 http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/1
442
443Even for a dummy PO msgset
444
445 >>> potmsgset = potemplate.getPOTMsgSetBySequence(20)
446 >>> translationmessage = potmsgset.getCurrentDummyTranslationMessage(
447 ... pofile.potemplate, pofile.language)
448 >>> print canonical_url(translationmessage)
449 http://launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/20
450
451Rosetta also has translation groups.
452
453 >>> canonical_url(getUtility(ITranslationGroupSet))
454 u'http://translations.launchpad.dev/+groups'
455
456
457== Specifications ==364== Specifications ==
458365
459 >>> from canonical.launchpad.interfaces import ISpecificationSet366 >>> from canonical.launchpad.interfaces import ISpecificationSet
460367
=== modified file 'lib/canonical/launchpad/doc/distribution-soyuz.txt'
--- lib/canonical/launchpad/doc/distribution-soyuz.txt 2009-08-14 12:59:56 +0000
+++ lib/canonical/launchpad/doc/distribution-soyuz.txt 2009-08-30 23:57:41 +0000
@@ -13,9 +13,10 @@
13distribution:13distribution:
1414
15 >>> from lp.registry.interfaces.distribution import IDistributionSet15 >>> from lp.registry.interfaces.distribution import IDistributionSet
16 >>> from lp.registry.interfaces.pocket import PackagePublishingPocket
16 >>> from canonical.launchpad.interfaces import (17 >>> from canonical.launchpad.interfaces import (
17 ... ISourcePackageName, IBinaryPackageName,18 ... ISourcePackageName, IBinaryPackageName,
18 ... PackagePublishingPocket, PackagePublishingStatus)19 ... PackagePublishingStatus)
1920
20 >>> distroset = getUtility(IDistributionSet)21 >>> distroset = getUtility(IDistributionSet)
21 >>> gentoo = distroset.getByName("gentoo")22 >>> gentoo = distroset.getByName("gentoo")
2223
=== modified file 'lib/canonical/launchpad/doc/hierarchical-menu.txt'
--- lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-08-26 09:33:33 +0000
+++ lib/canonical/launchpad/doc/hierarchical-menu.txt 2009-09-17 20:04:28 +0000
@@ -170,46 +170,6 @@
170 url='http://launchpad.dev/joy-of-cooking'170 url='http://launchpad.dev/joy-of-cooking'
171 text='Joy of cooking'>171 text='Joy of cooking'>
172172
173Breadcrumbs may have icons. The icon is only set for a breadcrumb if
174the builder's context has an IPathAdapter registration.
175
176 >>> from zope.traversing.interfaces import IPathAdapter
177 >>> from canonical.launchpad.webapp.tales import (
178 ... ObjectImageDisplayAPI)
179
180 # We need a custom image display adapter that overrides the
181 # the icon() method and returns an <img> tag.
182 >>> class RecipeImageDisplayAPI(ObjectImageDisplayAPI):
183 ... def icon(self):
184 ... return '<img src="/@@/recipe"/>'
185
186 >>> provideAdapter(
187 ... RecipeImageDisplayAPI, [IRecipe], IPathAdapter, 'image')
188
189 >>> breadcrumb = DynamicBreadcrumb(recipe)
190 >>> breadcrumb
191 <DynamicBreadcrumb
192 url='http://launchpad.dev/joy-of-cooking/spam'
193 text='Spam'
194 icon='<img src="/@@/recipe"/>'>
195
196The icon is not set if the default image adapter can not find an
197icon for the object.
198
199 # We'll use the default image adapter, which doesn't know about
200 # ICookbook objects.
201 >>> provideAdapter(
202 ... ObjectImageDisplayAPI, [ICookbook], IPathAdapter, 'image')
203
204 >>> print queryAdapter(cookbook, IPathAdapter, name='image').icon()
205 None
206
207 >>> breadcrumb = DynamicBreadcrumb(cookbook)
208 >>> breadcrumb
209 <DynamicBreadcrumb
210 url='http://launchpad.dev/joy-of-cooking'
211 text='Joy of cooking'>
212
213173
214== Customizing the hierarchy ==174== Customizing the hierarchy ==
215175
@@ -230,8 +190,7 @@
230 >>> spammy_hierarchy.items190 >>> spammy_hierarchy.items
231 [<TextualBreadcrumb191 [<TextualBreadcrumb
232 url='http://launchpad.dev/joy-of-cooking/spam'192 url='http://launchpad.dev/joy-of-cooking/spam'
233 text='Spam'193 text='Spam'>]
234 icon='<img src="/@@/recipe"/>'>]
235194
236195
237== Rendering the list ==196== Rendering the list ==
@@ -267,26 +226,3 @@
267226
268 >>> print_hierarchy(homepage_hierarchy.render())227 >>> print_hierarchy(homepage_hierarchy.render())
269 Location:228 Location:
270
271Breadcrumbs in the hierarchy that have icons are rendered with an <img>
272tag. Breadcrumbs without icons are not.
273
274 >>> breadcrumb_no_icon, breadcrumb_with_icon = hierarchy.items
275
276 >>> breadcrumb_no_icon
277 <TextualBreadcrumb
278 url='http://launchpad.dev/joy-of-cooking'
279 text='Joy of cooking'>
280
281 >>> breadcrumb_with_icon
282 <TextualBreadcrumb
283 url='http://launchpad.dev/joy-of-cooking/spam'
284 text='Spam'
285 icon='<img src="/@@/recipe"/>'>
286
287 >>> soup = BeautifulSoup(hierarchy.render())
288 >>> img_elems = soup.findAll('img')
289 >>> for img in img_elems:
290 ... print img
291 <img src="/@@/recipe" />
292
293229
=== modified file 'lib/canonical/launchpad/doc/launchbag.txt'
--- lib/canonical/launchpad/doc/launchbag.txt 2009-05-12 08:10:20 +0000
+++ lib/canonical/launchpad/doc/launchbag.txt 2009-09-03 00:08:57 +0000
@@ -57,6 +57,11 @@
57>>> print launchbag.login57>>> print launchbag.login
58None58None
5959
60'user' will also be set to None:
61
62>>> print launchbag.user
63None
64
60Let's do a cookie auth principal identification. In this case, the login65Let's do a cookie auth principal identification. In this case, the login
61will be cookie@example.com.66will be cookie@example.com.
6267
6368
=== modified file 'lib/canonical/launchpad/doc/launchpadview.txt'
--- lib/canonical/launchpad/doc/launchpadview.txt 2009-04-17 10:32:16 +0000
+++ lib/canonical/launchpad/doc/launchpadview.txt 2009-09-16 08:08:56 +0000
@@ -101,3 +101,13 @@
101 >>> view.error_message = structured('Information overload.')101 >>> view.error_message = structured('Information overload.')
102 >>> view.error_message.escapedtext102 >>> view.error_message.escapedtext
103 u'Information overload.'103 u'Information overload.'
104
105Every Launchpad view also knows whether edge redirection has been inhibited.
106
107 >>> view.isRedirectInhibited()
108 False
109 >>> new_request = TestRequest(HTTP_COOKIE="inhibit_beta_redirect=1")
110 >>> view = MyView(context, new_request)
111 >>> view.isRedirectInhibited()
112 True
113
104114
=== modified file 'lib/canonical/launchpad/doc/librarian.txt'
--- lib/canonical/launchpad/doc/librarian.txt 2009-08-07 12:54:05 +0000
+++ lib/canonical/launchpad/doc/librarian.txt 2009-09-15 02:18:25 +0000
@@ -78,7 +78,7 @@
7878
79 >>> from textwrap import dedent79 >>> from textwrap import dedent
80 >>> test_data = dedent("""80 >>> test_data = dedent("""
81 ... [vhosts]81 ... [librarian]
82 ... use_https: true82 ... use_https: true
83 ... """)83 ... """)
84 >>> config.push('test', test_data)84 >>> config.push('test', test_data)
@@ -689,8 +689,8 @@
689689
690== Time to last download ==690== Time to last download ==
691691
692The .last_downloaded property gives us the time delta from today to the day 692The .last_downloaded property gives us the time delta from today to the day
693that file was last downloaded, or None if it's never been downloaded. 693that file was last downloaded, or None if it's never been downloaded.
694694
695 >>> today = datetime.now(utc).date()695 >>> today = datetime.now(utc).date()
696 >>> public_file.last_downloaded == today - last_downloaded_date696 >>> public_file.last_downloaded == today - last_downloaded_date
697697
=== modified file 'lib/canonical/launchpad/doc/structural-subscriptions.txt'
--- lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-04-17 10:32:16 +0000
+++ lib/canonical/launchpad/doc/structural-subscriptions.txt 2009-08-25 11:21:05 +0000
@@ -160,8 +160,8 @@
160160
161 >>> evolution_sub.blueprint_notification_level = (161 >>> evolution_sub.blueprint_notification_level = (
162 ... BlueprintNotificationLevel.METADATA)162 ... BlueprintNotificationLevel.METADATA)
163 >>> evolution_package.removeBugSubscription(sampleperson)163 >>> evolution_package.removeBugSubscription(sampleperson, sampleperson)
164 >>> ubuntu.removeBugSubscription(sampleperson)164 >>> ubuntu.removeBugSubscription(sampleperson, sampleperson)
165 >>> syncUpdate(evolution_sub)165 >>> syncUpdate(evolution_sub)
166166
167Sample Person is no longer a subscriber to the package, but Foo Bar167Sample Person is no longer a subscriber to the package, but Foo Bar
168168
=== modified file 'lib/canonical/launchpad/doc/tales.txt'
--- lib/canonical/launchpad/doc/tales.txt 2009-08-20 12:24:29 +0000
+++ lib/canonical/launchpad/doc/tales.txt 2009-09-10 10:49:44 +0000
@@ -214,6 +214,19 @@
214 >>> test_tales('foo/fmt:shorten/8', foo='abcdefghij')214 >>> test_tales('foo/fmt:shorten/8', foo='abcdefghij')
215 'abcde...'215 'abcde...'
216216
217To ellipsize the middle of a string. use fmt:ellipsize and pass the max
218length.
219
220 >>> print test_tales('foo/fmt:ellipsize/25',
221 ... foo='foo-bar-baz-bazoo_22.443.tar.gz')
222 foo-bar-baz....443.tar.gz
223
224The string is not ellipsized if it is less than the max length.
225
226 >>> print test_tales('foo/fmt:ellipsize/25',
227 ... foo='firefox-0.9.2.orig.tar.gz')
228 firefox-0.9.2.orig.tar.gz
229
217To preserve newlines in text when displaying as HTML, use fmt:nl_to_br:230To preserve newlines in text when displaying as HTML, use fmt:nl_to_br:
218231
219 >>> test_tales('foo/fmt:nl_to_br',232 >>> test_tales('foo/fmt:nl_to_br',
@@ -333,6 +346,8 @@
333 * blueprint-branch links346 * blueprint-branch links
334 * projects347 * projects
335 * questions348 * questions
349 * distributions
350 * distroseries
336351
337352
338Person entries353Person entries
@@ -634,6 +649,22 @@
634 u'<a... class="sprite question">1:...</a>'649 u'<a... class="sprite question">1:...</a>'
635650
636651
652Distributions
653.............
654
655 >>> distribution = factory.makeDistribution()
656 >>> test_tales("distribution/fmt:link", distribution=distribution)
657 u'<a... class="sprite distribution">...</a>'
658
659
660Distribution Series
661...................
662
663 >>> distroseries = factory.makeDistroArchSeries().distroseries
664 >>> test_tales("distroseries/fmt:link", distroseries=distroseries)
665 u'<a href="...">...</a>'
666
667
637The fmt: namespace for specially formatted object info668The fmt: namespace for specially formatted object info
638------------------------------------------------------669------------------------------------------------------
639670
@@ -650,7 +681,7 @@
650The "standard" 'url' name is supported:681The "standard" 'url' name is supported:
651682
652 >>> test_tales("bugtracker/fmt:url", bugtracker=bugtracker)683 >>> test_tales("bugtracker/fmt:url", bugtracker=bugtracker)
653 u'/bugs/bugtrackers/email'684 u'http://bugs.launchpad.dev/bugs/bugtrackers/email'
654685
655(The url is relative if possible, and our test request claims to be from686(The url is relative if possible, and our test request claims to be from
656launchpad.dev, so the url is relative.)687launchpad.dev, so the url is relative.)
657688
=== modified file 'lib/canonical/launchpad/icing/style-3-0.css'
--- lib/canonical/launchpad/icing/style-3-0.css 2009-09-01 16:19:19 +0000
+++ lib/canonical/launchpad/icing/style-3-0.css 2009-09-18 03:48:00 +0000
@@ -7,7 +7,7 @@
7#maincontent {7#maincontent {
8 float: left;8 float: left;
9 width: 100%;9 width: 100%;
10 margin-right: -25em;10 margin-right: -25%;
11 }11 }
12#maincontent ol, #maincontent ul {12#maincontent ol, #maincontent ul {
13 padding-left: auto;13 padding-left: auto;
@@ -207,7 +207,7 @@
207 }207 }
208.footer {208.footer {
209 clear: both;209 clear: both;
210 margin-top: 2em;210 margin-top: 4em;
211 padding-top: 0.5em;211 padding-top: 0.5em;
212 }212 }
213.footer .lp-arcana {213.footer .lp-arcana {
@@ -230,14 +230,20 @@
230 }230 }
231.portlet, .aside {231.portlet, .aside {
232 clear: both;232 clear: both;
233 border-top: 1px solid #d6d6d6;233 border-top: 1px solid #EBEBEB;
234 padding: 0.5em 0;234 padding: 1em 0;
235 }235 }
236.top-portlet {236.top-portlet {
237 padding: 0 0 0.5em 0;237 padding: 0 0 0.5em 0;
238 margin: 0 0 1em;238 margin: 0 0 1em;
239 }239 }
240240.full-page-width {
241 z-index: 10;
242 width: 131%;
243 }
244.warning.message {
245 margin-top: 17px;
246 }
241/*247/*
242248
243Use percentages when setting font-size.249Use percentages when setting font-size.
@@ -397,6 +403,9 @@
397 color: #666;403 color: #666;
398 font-style: italic;404 font-style: italic;
399 }405 }
406li .registered {
407 font-style: normal;
408 }
400.description {409.description {
401 clear: both;410 clear: both;
402 font-size: 100%;411 font-size: 100%;
@@ -523,6 +532,15 @@
523 color: #747474;532 color: #747474;
524}533}
525534
535/* Registering slot */
536.registering {
537 float: right;
538 padding-top: 10px;
539 font-size: 85%;
540 color: #666;
541 font-style: italic;
542}
543
526/* Side content exceptions */544/* Side content exceptions */
527.side {545.side {
528 padding: 0.5em;546 padding: 0.5em;
@@ -546,9 +564,6 @@
546 background: #fbfbfb;564 background: #fbfbfb;
547 }565 }
548566
549.downloads a {
550 color: #4f843c;
551 }
552.downloads li {567.downloads li {
553 margin: 0;568 margin: 0;
554 padding: 2px 0 0;569 padding: 2px 0 0;
@@ -564,17 +579,20 @@
564 border-radius: 3px;579 border-radius: 3px;
565 background: #4f843c url(/@@/bg-project-downloads.png) center right no-repeat;580 background: #4f843c url(/@@/bg-project-downloads.png) center right no-repeat;
566 padding: 6%;581 padding: 6%;
567 padding-right: 50px;582 padding-right: 40px;
568 color: #fff;583 color: #fff;
569 font-size: 108%;584 font-size: 93%;
570 text-decoration: underline;585 }
586.downloads .version {
587 -moz-border-radius: 5px 5px 0 0;
588 -webkit-border-radius: 5px 5px 0 0;
589 -khtml-border-radius: 5px 5px 0 0;
590 border-radius: 5px 5px 0 0;
591 background: #d3e3c7;
592 padding: 0.2em 1em;
571 }593 }
572.downloads .released {594.downloads .released {
573 margin: .3em 0 1em 0;595 margin: .3em 0 .5em 0;
574 padding: 0 .2em 0 0;
575 text-align: right;
576 }
577.downloads .released span {
578 -moz-border-radius: 0 0 5px 5px;596 -moz-border-radius: 0 0 5px 5px;
579 -webkit-border-radius: 0 0 5px 5px;597 -webkit-border-radius: 0 0 5px 5px;
580 -khtml-border-radius: 0 0 5px 5px;598 -khtml-border-radius: 0 0 5px 5px;
@@ -583,7 +601,7 @@
583 padding: 0.2em 1em;601 padding: 0.2em 1em;
584 }602 }
585.downloads .alternate {603.downloads .alternate {
586 text-align: right;604 padding: 0 0 0 1em;
587 }605 }
588606
589ul.super-add-action {607ul.super-add-action {
@@ -610,11 +628,8 @@
610 text-decoration: underline;628 text-decoration: underline;
611 }629 }
612630
613.involvement ul {
614 border-top: 1px solid #d0d0d0;
615 }
616.involvement li {631.involvement li {
617 border-bottom: 1px solid #d0d0d0;632 border-top: 1px solid #d0d0d0;
618 padding: 0;633 padding: 0;
619 font-size: 108%;634 font-size: 108%;
620 font-weight: bold;635 font-weight: bold;
@@ -643,6 +658,10 @@
643 color: #5ba4c6;658 color: #5ba4c6;
644 background: url(/@@/blueprints-arrow-right.png) right center no-repeat;659 background: url(/@@/blueprints-arrow-right.png) right center no-repeat;
645 }660 }
661.involvement a:hover {
662 text-decoration: none;
663 background-color: #eee;
664 }
646665
647.announcements li {666.announcements li {
648 margin-bottom: 0.5em;667 margin-bottom: 0.5em;
@@ -662,6 +681,39 @@
662 margin-top: -2px;681 margin-top: -2px;
663 }682 }
664683
684/* For the Latest updates portlet
685 * at https://launchpad.dev/~cprov/+archive/ppa */
686ul.latest-ppa-updates .duration {
687 font-size: 75%;
688}
689ul.latest-ppa-updates li {
690 padding: 3px;
691 background-repeat: no-repeat;
692 background-position:right center;
693}
694/* The following could be generalised for use to the following selector:
695 * .side .portlet li.nth-child(odd)
696 * if needed. */
697ul.latest-ppa-updates li:nth-child(odd) {
698 border-top: 1px solid #dedede;
699 border-bottom: 1px solid #dedede;
700 background-color: #eeeeff;
701}
702ul.latest-ppa-updates li.FULLYBUILT {
703 background-image: url('/@@/yes');
704}
705ul.latest-ppa-updates li.FULLYBUILT_PENDING {
706 background-image: url('/@@/build-success-publishing');
707}
708ul.latest-ppa-updates li.NEEDSBUILD {
709 background-image: url('/@@/build-needed');
710}
711ul.latest-ppa-updates li.FAILEDTOBUILD {
712 background-image: url('/@@/build-failed');
713}
714ul.latest-ppa-updates li.BUILDING {
715 background-image: url('/@@/processing');
716}
665/* From nice_pre in tales.py */717/* From nice_pre in tales.py */
666pre.wrap {718pre.wrap {
667 white-space: -moz-pre-wrap;719 white-space: -moz-pre-wrap;
@@ -672,11 +724,22 @@
672724
673/* ==== Listing tables ==== */725/* ==== Listing tables ==== */
674726
675table.listing thead {
676 font-size: 116%;
677}
678table.listing th {727table.listing th {
679 font-weight: bold;728 font-weight: bold;
729}
730table.narrow-listing {
731 width: 45em;
732}
733/* ~person/+karma */
734table.cozy-listing {
735 width: 20em;
736 background-color: #fff;
737 border: 1px solid #d2d2d2;
738 border-bottom: 1px solid #d2d2d2;
739}
740table.cozy-listing td {
741 border: 1px #d2d2d2;
742 border-style: dotted none none none;
680}743}
681744
682ul.language, li.language {745ul.language, li.language {
@@ -688,57 +751,89 @@
688/* ==== Translations hand-made forms ==== */751/* ==== Translations hand-made forms ==== */
689752
690form.translations div.fields {753form.translations div.fields {
691 padding: 1em;754 padding: 1em;
692}755}
693756
694form.translations div.actions {757form.translations div.actions {
695 padding: 1em;758 padding: 1em;
696 text-align: right;759 text-align: right;
760 clear:both;
697}761}
698762
699form.translations input {763form.translations input {
700 padding-left: 0.5em;764 padding-left: 0.5em;
701 padding-right: 0.5em;765 padding-right: 0.5em;
702}766}
703767
704form.translations select {768form.translations select {
705 margin-left: 0.5em;769 margin-left: 0.5em;
706 padding-right: 0.5em;770 padding-right: 0.5em;
707}771}
708772
709form.translations label {773form.translations label {
710 padding-left: 0.5em;774 padding-left: 0.5em;
711 padding-right: 1em;775 padding-right: 1em;
712}776}
713777
714form.translations .listbox label {778form.translations .listbox label {
715 padding: 2px 1em 2px 2px;779 padding: 2px 1em 2px 2px;
716}780}
717781
718/* Provide top-alignment for radio boxes and longer explanations782/* Provide top-alignment for radio boxes and longer explanations
719 * without using tables.783 * without using tables.
784 *
785 * Examples:
786 * https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/es/+upload
787 * https://translations.launchpad.dev/evolution/trunk/+pots/evolution-2.2/+export
720 */788 */
721form.translations div.fields div.alignment {
722 position: relative;
723}
724form.translations div.alignment div.top-alignment {
725 position: absolute;
726 top: 0px;
727}
728form.translations div.alignment .spacer {
729 visibility: hidden;
730}
731form.translations div.alignment .content {789form.translations div.alignment .content {
732 display: inline-block;790 float:left;
733 margin-left: 0px;791}
734}792form.translations div.alignment .selector {
735793 margin-right: 0.5em;
794 float: left;
795 clear: both;
796}
736form.translations div.alignment .content label {797form.translations div.alignment .content label {
737 padding: 0px;798 padding: 0px;
738 margin: 0px;799 margin: 0px;
739 font-weight: bold;800 font-weight: bold;
740}801}
741802form.translations div.alignment .secondary label {
742table.narrow-listing {803 font-weight: normal;
743 width: 45em;804 padding: 2px 1em 2px 2px;
805}
806
807/* Translations statistics and legend.
808 *
809 * Examples:
810 * https://translations.launchpad.dev/ubuntu/hoary/+lang/es
811 * https://translations.launchpad.dev/evolution/trunk/+lang/es
812 */
813
814div.translations-legend {
815 padding-top: 2em;
816 padding-bottom: 1em;
817}
818table.translation-stats td {
819 text-align:center;
820}
821table.translation-stats td.template-name {
822 text-align:left;
823}
824table.translation-stats tfoot td,
825table.translation-stats tfoot th {
826 background-color: #f7f7f7;
827 border: 0px;
828 border-top: 2px solid #d2d2d2;
829 border-bottom: 2px solid #d2d2d2;
830 padding-top: 5px;
831 padding-bottom: 5px;
832 font-weight: bold;
833}
834table.translation-stats tfoot th {
835 text-align:left;
836}
837table.translation-stats tfoot td {
838 text-align:center;
744}839}
745840
=== modified file 'lib/canonical/launchpad/icing/style.css'
--- lib/canonical/launchpad/icing/style.css 2009-08-31 00:06:29 +0000
+++ lib/canonical/launchpad/icing/style.css 2009-09-18 13:59:17 +0000
@@ -141,7 +141,7 @@
141.bullet {background:url(icon-sprites-2) 0 -1024px no-repeat;}141.bullet {background:url(icon-sprites-2) 0 -1024px no-repeat;}
142.zoom-in {background:url(icon-sprites-2) 0 -1056px no-repeat;}142.zoom-in {background:url(icon-sprites-2) 0 -1056px no-repeat;}
143.zoom-out {background:url(icon-sprites-2) 0 -1088px no-repeat;}143.zoom-out {background:url(icon-sprites-2) 0 -1088px no-repeat;}
144.arquitecture {background:url(icon-sprites-2) 0 -1120px no-repeat;}144.architecture {background:url(icon-sprites-2) 0 -1120px no-repeat;}
145.ppa-icon {background:url(icon-sprites-2) 0 -1147px no-repeat;}145.ppa-icon {background:url(icon-sprites-2) 0 -1147px no-repeat;}
146.ppa-icon-inactive {background:url(icon-sprites-2) 0 -1171px no-repeat;}146.ppa-icon-inactive {background:url(icon-sprites-2) 0 -1171px no-repeat;}
147147
@@ -308,7 +308,6 @@
308 margin: 0 0 0.5em;308 margin: 0 0 0.5em;
309 /* Content headings are highlighted using a different color (the color is309 /* Content headings are highlighted using a different color (the color is
310 overriden per-application in subsequent rules) instead of bold text. */310 overriden per-application in subsequent rules) instead of bold text. */
311 color: #83ad23;
312 font-weight: normal;311 font-weight: normal;
313}312}
314h1 {font-size: 2em;}313h1 {font-size: 2em;}
@@ -675,30 +674,11 @@
675674
676/* === Universal page layout === */675/* === Universal page layout === */
677676
678/* All pages begin with a global header or a topline. */677/* All pages include the sitemessage in the footer if one is defined
679#globalheader {678 in the config. */
680 clear: both;679.sitemessage {
681 position: relative;680 font-size: 13px;
682 width: 100%;681 text-align: right;
683 height: 21px;
684 overflow: hidden;
685 margin: 0 0 1em 0;
686 padding: 0;
687 background-color: #EEE;
688 background-image: url(globalheader_bg.gif);
689 background-repeat: repeat-x;
690 background-position: top center;
691}
692#globalheader .sitemessage a {
693 color: #FFF;
694 text-decoration: underline;
695}
696/* Site-specific message. */
697#globalheader .sitemessage {
698 text-align: center;
699 color: #FFF;
700 font-size: 13px;
701 padding-top: 2px;
702}682}
703683
704/* === Login control === */684/* === Login control === */
@@ -729,7 +709,8 @@
729 margin-left: 0;709 margin-left: 0;
730 list-style-type: none;710 list-style-type: none;
731 clear: both;711 clear: both;
732 margin-bottom: 0.5em;712 margin-bottom: 1em;
713 font-size: 80%;
733}714}
734ol.breadcrumbs li {715ol.breadcrumbs li {
735 display:inline;716 display:inline;
@@ -1492,11 +1473,16 @@
1492.boardCommentDetails table {1473.boardCommentDetails table {
1493 margin: -0.5em -12px;1474 margin: -0.5em -12px;
1494}1475}
1476.boardCommentDetails tr {
1477 width: 100%;
1478}
1495.boardCommentDetails td {1479.boardCommentDetails td {
1480 width:98%;
1496 padding: 0.5em 12px;1481 padding: 0.5em 12px;
1497}1482}
1498.boardCommentDetails td.bug-comment-index {1483.boardCommentDetails td.bug-comment-index {
1499 border-right: 1px solid #ddd;1484 width:2%;
1485 border-left: 1px solid #ddd;
1500}1486}
1501.boardCommentBody {padding: 0.5em 12px 0;}1487.boardCommentBody {padding: 0.5em 12px 0;}
1502.boardBugActivityBody {1488.boardBugActivityBody {
@@ -1687,17 +1673,8 @@
1687 border: 1px solid gray;1673 border: 1px solid gray;
1688 padding: 0.3em;1674 padding: 0.3em;
1689 margin: 1em 0 1em 0;1675 margin: 1em 0 1em 0;
1690 float: left; /* So the border doesn't use 100% of the page */1676}
1691}1677
1692
1693div#build-status-summary {
1694 clear:right;
1695 float:right;
1696}
1697
1698div#build-status-summary h2 {
1699 margin-top:0;
1700}
17011678
1702div#build-status-summary th {1679div#build-status-summary th {
1703 text-align:left;1680 text-align:left;
@@ -1762,6 +1739,30 @@
1762 padding-left: 1em;1739 padding-left: 1em;
1763}1740}
17641741
1742/* PPA installation instructions slider.*/
1743#pre-karmic-systems-slide-trigger {
1744 cursor: pointer;
1745 color: #093; /* for the underline on hover */
1746}
1747#pre-karmic-systems-slide-trigger:hover {
1748 text-decoration: underline
1749}
1750#ppa-install-slide-trigger {
1751 cursor: pointer;
1752 color: #093; /* for the underline on hover */
1753}
1754#ppa-install-slide-trigger:hover {
1755 text-decoration: underline
1756}
1757#ppa-install .widget-body {
1758 margin: 1em;
1759}
1760#ppa-install .widget-body.lazr-closed {
1761 height: 0px;
1762 overflow: hidden;
1763 display: block;
1764}
1765
1765/* Brand the related PPA versions with the PPA icon (not currently1766/* Brand the related PPA versions with the PPA icon (not currently
1766 in the sprite image.) */1767 in the sprite image.) */
1767div#slide-trigger {1768div#slide-trigger {
@@ -1831,6 +1832,13 @@
1831 max-width: 100%;1832 max-width: 100%;
1832}1833}
18331834
1835/* <pre> for presenting source changelog nicely. */
1836pre.changelog {
1837 margin: 0.5em;
1838 padding: 5px;
1839 font: 116% monospace;
1840}
1841
1834/* Disabled PPAs when linkified (uploaders) should be gray and when not1842/* Disabled PPAs when linkified (uploaders) should be gray and when not
1835 linkified should be lined-through. */1843 linkified should be lined-through. */
1836a.ppa-icon-inactive {1844a.ppa-icon-inactive {
@@ -1844,6 +1852,18 @@
1844 padding: 1em 1em 2em 0em;1852 padding: 1em 1em 2em 0em;
1845}1853}
18461854
1855/* XXX Michael Nelson 20090828 bug=420376
1856 * Temporary style for actions on the ppa/+packages page until we move
1857 * the copy/delete packages functionality into the page itself. */
1858div.ppa-packaging-tmp-actions {
1859 float:right;
1860}
1861
1862div.ppa-packaging-tmp-actions .portlet {
1863 border-top: 0 none;
1864}
1865
1866
1847/* --- Code --- */1867/* --- Code --- */
18481868
1849body.tab-branches #applications {background: url(app-code-wm.gif) no-repeat;}1869body.tab-branches #applications {background: url(app-code-wm.gif) no-repeat;}
@@ -1920,6 +1940,7 @@
1920#branch-pending-writes {1940#branch-pending-writes {
1921 background: #FFF59C;1941 background: #FFF59C;
1922 width: 40em;1942 width: 40em;
1943 margin: 1em auto;
1923 padding-left: 1em;1944 padding-left: 1em;
1924 padding-right: 1em;1945 padding-right: 1em;
1925 padding-top: 0.2em;1946 padding-top: 0.2em;
19261947
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-08-24 12:06:17 +0000
+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2009-09-09 11:28:03 +0000
@@ -22,6 +22,8 @@
22 patch_collection_return_type, patch_plain_parameter_type,22 patch_collection_return_type, patch_plain_parameter_type,
23 patch_choice_parameter_type, patch_reference_property)23 patch_choice_parameter_type, patch_reference_property)
2424
25from canonical.launchpad.interfaces.structuralsubscription import (
26 IStructuralSubscription, IStructuralSubscriptionTarget)
25from lp.bugs.interfaces.bug import IBug27from lp.bugs.interfaces.bug import IBug
26from lp.bugs.interfaces.bugbranch import IBugBranch28from lp.bugs.interfaces.bugbranch import IBugBranch
27from lp.bugs.interfaces.bugnomination import IBugNomination29from lp.bugs.interfaces.bugnomination import IBugNomination
@@ -46,6 +48,7 @@
46from lp.registry.interfaces.distroseries import IDistroSeries48from lp.registry.interfaces.distroseries import IDistroSeries
47from lp.registry.interfaces.person import IPerson, IPersonPublic49from lp.registry.interfaces.person import IPerson, IPersonPublic
48from canonical.launchpad.interfaces.hwdb import HWBus, IHWSubmission50from canonical.launchpad.interfaces.hwdb import HWBus, IHWSubmission
51from lp.registry.interfaces.pocket import PackagePublishingPocket
49from lp.registry.interfaces.product import IProduct52from lp.registry.interfaces.product import IProduct
50from lp.registry.interfaces.productseries import IProductSeries53from lp.registry.interfaces.productseries import IProductSeries
51from lp.soyuz.interfaces.archive import IArchive54from lp.soyuz.interfaces.archive import IArchive
@@ -59,7 +62,7 @@
59from lp.soyuz.interfaces.publishing import (62from lp.soyuz.interfaces.publishing import (
60 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,63 IBinaryPackagePublishingHistory, ISecureBinaryPackagePublishingHistory,
61 ISecureSourcePackagePublishingHistory, ISourcePackagePublishingHistory,64 ISecureSourcePackagePublishingHistory, ISourcePackagePublishingHistory,
62 PackagePublishingPocket, PackagePublishingStatus)65 PackagePublishingStatus)
63from lp.soyuz.interfaces.packageset import IPackageset66from lp.soyuz.interfaces.packageset import IPackageset
64from lp.soyuz.interfaces.queue import (67from lp.soyuz.interfaces.queue import (
65 IPackageUpload, PackageUploadCustomFormat, PackageUploadStatus)68 IPackageUpload, PackageUploadCustomFormat, PackageUploadStatus)
@@ -277,3 +280,11 @@
277IPackageUpload['pocket'].vocabulary = PackagePublishingPocket280IPackageUpload['pocket'].vocabulary = PackagePublishingPocket
278patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)281patch_reference_property(IPackageUpload, 'distroseries', IDistroSeries)
279patch_reference_property(IPackageUpload, 'archive', IArchive)282patch_reference_property(IPackageUpload, 'archive', IArchive)
283
284# IStructuralSubscription
285patch_reference_property(
286 IStructuralSubscription, 'target', IStructuralSubscriptionTarget)
287
288patch_reference_property(
289 IStructuralSubscriptionTarget, 'parent_subscription_target',
290 IStructuralSubscriptionTarget)
280291
=== modified file 'lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt'
--- lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2008-02-11 17:44:30 +0000
+++ lib/canonical/launchpad/interfaces/ftests/structural-subscription-target.txt 2009-08-25 11:21:05 +0000
@@ -48,31 +48,100 @@
48 >>> target.addBugSubscription(no_priv, no_priv)48 >>> target.addBugSubscription(no_priv, no_priv)
49 <StructuralSubscription ...>49 <StructuralSubscription ...>
5050
51Let's add an ITeam as one of the subscribers:51People can only be subscribed by themselves, and only the team admins may
52subscribe a team.
53
54no-priv, who has no relationship to ubuntu-team, cannot subscribe it.
5255
53 >>> ubuntu_team = personset.getByName("ubuntu-team")56 >>> ubuntu_team = personset.getByName("ubuntu-team")
54 >>> target.addBugSubscription(ubuntu_team, ubuntu_team)57 >>> target.addBugSubscription(ubuntu_team, no_priv)
55 <StructuralSubscription ...>58 Traceback (most recent call last):
5659 ...
57 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])60 UserCannotSubscribePerson: no-priv does not have permission to subscribe ubuntu-team.
58 [u'no-priv', u'ubuntu-team']61
62But kamion, an admin of the team, can.
63
64 >>> kamion = personset.getByName("kamion")
65 >>> target.addBugSubscription(ubuntu_team, kamion)
66 <StructuralSubscription ...>
67
68 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
69 [u'no-priv', u'ubuntu-team']
70
71foobar, a Launchpad administrator, can as well.
72
73 >>> foobar = personset.getByName("name16")
74 >>> target.addBugSubscription(ubuntu_team, foobar)
75 <StructuralSubscription ...>
76
77A non-admin cannot subscribe a person other than themselves.
78
79 >>> target.addBugSubscription(kamion, no_priv)
80 Traceback (most recent call last):
81 ...
82 UserCannotSubscribePerson: no-priv does not have permission to subscribe kamion.
83 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
84 [u'no-priv', u'ubuntu-team']
85
86But again, an admin can.
87
88 >>> target.addBugSubscription(kamion, foobar)
89 <StructuralSubscription ...>
90 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
91 [u'kamion', u'no-priv', u'ubuntu-team']
5992
60To remove a bug subscription, use93To remove a bug subscription, use
61IStructuralSubscriptionTarget.removeBugSubscription:94IStructuralSubscriptionTarget.removeBugSubscription:
6295
63 >>> target.removeBugSubscription(no_priv)96 >>> target.removeBugSubscription(no_priv, no_priv)
64 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])97 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
65 [u'ubuntu-team']98 [u'kamion', u'ubuntu-team']
99
100The subscription rules apply to unsubscription as well.
101
102An unprivileged user cannot unsubscribe a team.
103
104 >>> target.removeBugSubscription(ubuntu_team, no_priv)
105 Traceback (most recent call last):
106 ...
107 UserCannotSubscribePerson: no-priv does not have permission to unsubscribe ubuntu-team.
108 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
109 [u'kamion', u'ubuntu-team']
110
111But a team admin can.
112
113 >>> target.removeBugSubscription(ubuntu_team, kamion)
114 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
115 [u'kamion']
116
117An unprivileged user also cannot unsubscribe another user.
118
119 >>> target.removeBugSubscription(kamion, no_priv)
120 Traceback (most recent call last):
121 ...
122 UserCannotSubscribePerson: no-priv does not have permission to unsubscribe kamion.
123 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
124 [u'kamion']
125
126But the user themselves can.
127
128 >>> target.removeBugSubscription(kamion, kamion)
129 >>> sorted([sub.subscriber.name for sub in target.bug_subscriptions])
130 []
66131
67Trying to remove a subscription that doesn't exist on a target raises a132Trying to remove a subscription that doesn't exist on a target raises a
68DeleteSubscriptionError.133DeleteSubscriptionError.
69134
70 >>> foobar = personset.getByName("name16")135 >>> target.removeBugSubscription(foobar, foobar)
71 >>> target.removeBugSubscription(foobar)
72 Traceback (most recent call last):136 Traceback (most recent call last):
73 ...137 ...
74 DeleteSubscriptionError: ...138 DeleteSubscriptionError: ...
75139
140Let's subscribe ubuntu-team again.
141
142 >>> target.addBugSubscription(ubuntu_team, foobar)
143 <StructuralSubscription ...>
144
76Trying to remove a bug subscription when notification levels for other145Trying to remove a bug subscription when notification levels for other
77applications are set, doesn't remove the subscription. Instead the146applications are set, doesn't remove the subscription. Instead the
78notification level for bugs is set to NOTHING.147notification level for bugs is set to NOTHING.
@@ -93,7 +162,7 @@
93 >>> print_subscriptions_list(target.getSubscriptions())162 >>> print_subscriptions_list(target.getSubscriptions())
94 name16 COMMENTS LIFECYCLE163 name16 COMMENTS LIFECYCLE
95 ubuntu-team COMMENTS NOTHING164 ubuntu-team COMMENTS NOTHING
96 >>> target.removeBugSubscription(foobar)165 >>> target.removeBugSubscription(foobar, foobar)
97 >>> print_subscriptions_list(target.getSubscriptions())166 >>> print_subscriptions_list(target.getSubscriptions())
98 name16 NOTHING LIFECYCLE167 name16 NOTHING LIFECYCLE
99 ubuntu-team COMMENTS NOTHING168 ubuntu-team COMMENTS NOTHING
100169
=== modified file 'lib/canonical/launchpad/interfaces/structuralsubscription.py'
--- lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-07-17 00:26:05 +0000
+++ lib/canonical/launchpad/interfaces/structuralsubscription.py 2009-09-09 14:26:18 +0000
@@ -13,7 +13,8 @@
13 'DeleteSubscriptionError',13 'DeleteSubscriptionError',
14 'IStructuralSubscription',14 'IStructuralSubscription',
15 'IStructuralSubscriptionForm',15 'IStructuralSubscriptionForm',
16 'IStructuralSubscriptionTarget'16 'IStructuralSubscriptionTarget',
17 'UserCannotSubscribePerson',
17 ]18 ]
1819
19from zope.interface import Attribute, Interface20from zope.interface import Attribute, Interface
@@ -23,6 +24,14 @@
23from canonical.launchpad import _24from canonical.launchpad import _
24from canonical.launchpad.fields import (25from canonical.launchpad.fields import (
25 ParticipatingPersonChoice, PublicPersonChoice)26 ParticipatingPersonChoice, PublicPersonChoice)
27from lp.registry.interfaces.person import IPerson
28
29from lazr.restful.declarations import (
30 REQUEST_USER, call_with, exported, export_as_webservice_entry,
31 export_factory_operation, export_read_operation, export_write_operation,
32 operation_parameters, operation_returns_collection_of,
33 operation_returns_entry, webservice_error)
34from lazr.restful.fields import Reference
2635
2736
28class BugNotificationLevel(DBEnumeratedType):37class BugNotificationLevel(DBEnumeratedType):
@@ -83,6 +92,7 @@
8392
84class IStructuralSubscription(Interface):93class IStructuralSubscription(Interface):
85 """A subscription to a Launchpad structure."""94 """A subscription to a Launchpad structure."""
95 export_as_webservice_entry()
8696
87 id = Int(title=_('ID'), readonly=True, required=True)97 id = Int(title=_('ID'), readonly=True, required=True)
88 product = Int(title=_('Product'), required=False, readonly=True)98 product = Int(title=_('Product'), required=False, readonly=True)
@@ -95,13 +105,13 @@
95 title=_('Distribution series'), required=False, readonly=True)105 title=_('Distribution series'), required=False, readonly=True)
96 sourcepackagename = Int(106 sourcepackagename = Int(
97 title=_('Source package name'), required=False, readonly=True)107 title=_('Source package name'), required=False, readonly=True)
98 subscriber = ParticipatingPersonChoice(108 subscriber = exported(ParticipatingPersonChoice(
99 title=_('Subscriber'), required=True, vocabulary='ValidPersonOrTeam',109 title=_('Subscriber'), required=True, vocabulary='ValidPersonOrTeam',
100 readonly=True, description=_("The person subscribed."))110 readonly=True, description=_("The person subscribed.")))
101 subscribed_by = PublicPersonChoice(111 subscribed_by = exported(PublicPersonChoice(
102 title=_('Subscribed by'), required=True,112 title=_('Subscribed by'), required=True,
103 vocabulary='ValidPersonOrTeam', readonly=True,113 vocabulary='ValidPersonOrTeam', readonly=True,
104 description=_("The person creating the subscription."))114 description=_("The person creating the subscription.")))
105 bug_notification_level = Choice(115 bug_notification_level = Choice(
106 title=_("Bug notification level"), required=True,116 title=_("Bug notification level"), required=True,
107 vocabulary=BugNotificationLevel,117 vocabulary=BugNotificationLevel,
@@ -114,19 +124,30 @@
114 default=BlueprintNotificationLevel.NOTHING,124 default=BlueprintNotificationLevel.NOTHING,
115 description=_("The volume and type of blueprint notifications "125 description=_("The volume and type of blueprint notifications "
116 "this subscription will generate."))126 "this subscription will generate."))
117 date_created = Datetime(127 date_created = exported(Datetime(
118 title=_("The date on which this subscription was created."),128 title=_("The date on which this subscription was created."),
119 required=False)129 required=False, readonly=True))
120 date_last_updated = Datetime(130 date_last_updated = exported(Datetime(
121 title=_("The date on which this subscription was last updated."),131 title=_("The date on which this subscription was last updated."),
122 required=False)132 required=False, readonly=True))
123133
124 target = Attribute("The structure to which this subscription belongs.")134 target = exported(Reference(
135 schema=Interface, # IStructuralSubscriptionTarget
136 required=True, readonly=True,
137 title=_("The structure to which this subscription belongs.")))
125138
126139
127class IStructuralSubscriptionTarget(Interface):140class IStructuralSubscriptionTarget(Interface):
128 """A Launchpad Structure allowing users to subscribe to it."""141 """A Launchpad Structure allowing users to subscribe to it."""
142 export_as_webservice_entry()
129143
144 # We don't really want to expose the level details yet. Only
145 # BugNotificationLevel.COMMENTS is used at this time.
146 @call_with(
147 min_bug_notification_level=BugNotificationLevel.COMMENTS,
148 min_blueprint_notification_level=BlueprintNotificationLevel.NOTHING)
149 @operation_returns_collection_of(IStructuralSubscription)
150 @export_read_operation()
130 def getSubscriptions(min_bug_notification_level,151 def getSubscriptions(min_bug_notification_level,
131 min_blueprint_notification_level):152 min_blueprint_notification_level):
132 """Return all the subscriptions with the specified levels.153 """Return all the subscriptions with the specified levels.
@@ -148,11 +169,21 @@
148 This method is used to create a new `IStructuralSubscription`169 This method is used to create a new `IStructuralSubscription`
149 for the target, with no levels set.170 for the target, with no levels set.
150171
151 :subscriber: The IPerson who will be subscribed.172 :subscriber: The IPerson who will be subscribed. If omitted,
173 subscribed_by will be used.
152 :subscribed_by: The IPerson creating the subscription.174 :subscribed_by: The IPerson creating the subscription.
153 :return: The new subscription.175 :return: The new subscription.
154 """176 """
155177
178 @operation_parameters(
179 subscriber=Reference(
180 schema=IPerson,
181 title=_(
182 'Person to subscribe. If omitted, the requesting user will be'
183 ' subscribed.'),
184 required=False))
185 @call_with(subscribed_by=REQUEST_USER)
186 @export_factory_operation(IStructuralSubscription, [])
156 def addBugSubscription(subscriber, subscribed_by):187 def addBugSubscription(subscriber, subscribed_by):
157 """Add a bug subscription for this structure.188 """Add a bug subscription for this structure.
158189
@@ -160,21 +191,36 @@
160 for the target with the bug notification level set to191 for the target with the bug notification level set to
161 COMMENTS, the only level currently in use.192 COMMENTS, the only level currently in use.
162193
163 :subscriber: The IPerson who will be subscribed.194 :subscriber: The IPerson who will be subscribed. If omitted,
195 subscribed_by will be used.
164 :subscribed_by: The IPerson creating the subscription.196 :subscribed_by: The IPerson creating the subscription.
165 :return: The new bug subscription.197 :return: The new bug subscription.
166 """198 """
167199
168 def removeBugSubscription(subscriber):200 @operation_parameters(
201 subscriber=Reference(
202 schema=IPerson,
203 title=_(
204 'Person to unsubscribe. If omitted, the requesting user will '
205 'be unsubscribed.'),
206 required=False))
207 @call_with(unsubscribed_by=REQUEST_USER)
208 @export_write_operation()
209 def removeBugSubscription(subscriber, unsubscribed_by):
169 """Remove a subscription to bugs from this structure.210 """Remove a subscription to bugs from this structure.
170211
171 If subscription levels for other applications are set,212 If subscription levels for other applications are set,
172 set the subscription's `bug_notification_level` to213 set the subscription's `bug_notification_level` to
173 `NOTHING`, otherwise, destroy the subscription.214 `NOTHING`, otherwise, destroy the subscription.
174215
175 :subscriber: The IPerson who will be subscribed.216 :subscriber: The IPerson who will be unsubscribed. If omitted,
217 unsubscribed_by will be used.
218 :unsubscribed_by: The IPerson removing the subscription.
176 """219 """
177220
221 @operation_parameters(person=Reference(schema=IPerson))
222 @operation_returns_entry(IStructuralSubscription)
223 @export_read_operation()
178 def getSubscription(person):224 def getSubscription(person):
179 """Return the subscription for `person`, if it exists."""225 """Return the subscription for `person`, if it exists."""
180226
@@ -207,3 +253,9 @@
207253
208 Raised when an error occurred trying to delete a254 Raised when an error occurred trying to delete a
209 structural subscription."""255 structural subscription."""
256 webservice_error(400)
257
258
259class UserCannotSubscribePerson(Exception):
260 """User does not have permission to subscribe the person or team."""
261 webservice_error(401)
210262
=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-08-26 20:50:26 +0000
+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-09-02 22:13:06 +0000
@@ -1211,6 +1211,7 @@
1211 var bugtarget_content = Y.get('#bugtarget-picker-' + conf.row_id);1211 var bugtarget_content = Y.get('#bugtarget-picker-' + conf.row_id);
1212 var status_content = tr.query('.status-content');1212 var status_content = tr.query('.status-content');
1213 var importance_content = tr.query('.importance-content');1213 var importance_content = tr.query('.importance-content');
1214 var assignee_content = Y.get('#assignee-picker-' + conf.row_id);
1214 var milestone_content = tr.query('.milestone-content');1215 var milestone_content = tr.query('.milestone-content');
12151216
1216 if (Y.Lang.isValue(LP.client.cache.bug) &&1217 if (Y.Lang.isValue(LP.client.cache.bug) &&
@@ -1357,6 +1358,20 @@
1357 milestone_choice_edit);1358 milestone_choice_edit);
1358 milestone_choice_edit.render(); 1359 milestone_choice_edit.render();
1359 }1360 }
1361 if (Y.Lang.isValue(assignee_content)) {
1362 var assignee_picker = Y.lp.picker.addPickerPatcher(
1363 'ValidAssignee',
1364 conf.bugtask_path,
1365 "assignee_link",
1366 assignee_content.get('id'),
1367 true,
1368 true,
1369 {"step_title": "Search for people or teams",
1370 "header": "Change assignee",
1371 "remove_button_text": "Remove asignee",
1372 "null_display_value": "Unassigned"});
1373 assignee_picker.render()
1374 }
1360};1375};
13611376
1362/**1377/**
13631378
=== modified file 'lib/canonical/launchpad/javascript/registry/tests/milestone_table.html'
--- lib/canonical/launchpad/javascript/registry/tests/milestone_table.html 2009-06-19 18:31:25 +0000
+++ lib/canonical/launchpad/javascript/registry/tests/milestone_table.html 2009-09-14 15:03:45 +0000
@@ -22,7 +22,7 @@
22 <table id="series-trunk" class="listing">22 <table id="series-trunk" class="listing">
23 <thead>23 <thead>
24 <tr>24 <tr>
25 <th>Version "Codename"</th>25 <th>Version</th>
26 </tr>26 </tr>
27 </thead>27 </thead>
28 <tbody id="milestone-rows">28 <tbody id="milestone-rows">
2929
=== modified file 'lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js'
--- lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js 2009-06-30 21:06:27 +0000
+++ lib/canonical/launchpad/javascript/soyuz/update_archive_build_statuses.js 2009-08-28 16:06:56 +0000
@@ -19,9 +19,9 @@
19 var lp_client = new LP.client.Launchpad();19 var lp_client = new LP.client.Launchpad();
2020
21 /**21 /**
22 * Configuration for the dynamic update of the build summary table22 * Configuration for the dynamic update of the build summary portlet.
23 */23 */
24 var build_summary_table_dynamic_update_config = {24 var build_summary_portlet_dynamic_update_config = {
25 uri: null, // Note: we have to defer setting the uri until later as25 uri: null, // Note: we have to defer setting the uri until later as
26 // the LP.client.cache is not initialized until the end26 // the LP.client.cache is not initialized until the end
27 // of the page.27 // of the page.
@@ -30,19 +30,19 @@
3030
31 /**31 /**
32 * This function knows how to update an Archive Build Status summary32 * This function knows how to update an Archive Build Status summary
33 * table, when given an object of the form:33 * when given an object of the form:
34 * {total: 5, failed: 3}34 * {total: 5, failed: 3}
35 *35 *
36 * @config domUpdateFunction36 * @config domUpdateFunction
37 */37 */
38 domUpdateFunction: function(table_node, data_object){38 domUpdateFunction: function(portlet_node, data_object){
39 var td_nodelist = table_node.getElementsByTagName('td');39 var counter_nodelist = portlet_node.queryAll('.build-count');
4040
41 // For each node of the table's td elements41 // For each node of the counter node in the portlet:
42 td_nodelist.each(function(node){42 counter_nodelist.each(function(node){
43 // Check whether the node has a class matching the data name43 // Check whether the node has a class matching the data name
44 // of the passed in data, and if so, set the innerHTML to44 // of the passed in data, and if so, set the innerHTML to
45 // thecorresponding value.45 // the corresponding value.
46 Y.each(data_object, function(data_value, data_name){46 Y.each(data_object, function(data_value, data_name){
47 if (node.hasClass(data_name)){47 if (node.hasClass(data_name)){
48 previous_value = node.get("innerHTML");48 previous_value = node.get("innerHTML");
@@ -67,9 +67,9 @@
67 *67 *
68 * @config stopUpdatesCheckFunction68 * @config stopUpdatesCheckFunction
69 */69 */
70 stopUpdatesCheckFunction: function(table_node){70 stopUpdatesCheckFunction: function(portlet_node){
71 // Stop updating only when there are zero pending builds:71 // Stop updating only when there are zero pending builds:
72 var pending_elem = table_node.query("td.pending");72 var pending_elem = portlet_node.query(".pending");
73 if (pending_elem === null){73 if (pending_elem === null){
74 return true;74 return true;
75 }75 }
@@ -83,13 +83,13 @@
83 * Initialization of the build count summary dynamic table updates.83 * Initialization of the build count summary dynamic table updates.
84 */84 */
85 Y.on("domready", function(){85 Y.on("domready", function(){
86 // Grab the Archive build count table and tell it how to86 // Grab the Archive build count portlet and tell it how to
87 // update itself:87 // update itself:
88 var table = Y.get('table#build-count-table');88 var portlet = Y.get('div#build-status-summary');
89 build_summary_table_dynamic_update_config.uri =89 build_summary_portlet_dynamic_update_config.uri =
90 LP.client.cache.context.self_link;90 LP.client.cache.context.self_link;
91 table.plug(Y.lp.DynamicDomUpdater,91 portlet.plug(Y.lp.DynamicDomUpdater,
92 build_summary_table_dynamic_update_config);92 build_summary_portlet_dynamic_update_config);
93 });93 });
9494
95 /**95 /**
9696
=== modified file 'lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt'
--- lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2009-08-30 13:31:02 +0000
+++ lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2009-09-17 15:24:12 +0000
@@ -53,7 +53,8 @@
53 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))53 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
54 &bull;...Launchpad...) devmode demo site54 &bull;...Launchpad...) devmode demo site
5555
56 >>> print extract_text(find_tag_by_id(browser.contents, 'globalheader'))56 >>> print extract_text(find_tags_by_class(
57 ... browser.contents, 'sitemessage')[0])
57 This is a demo site mmk. File a bug.58 This is a demo site mmk. File a bug.
58 >>> print browser.getLink(url="http://example.com").text59 >>> print browser.getLink(url="http://example.com").text
59 File a bug60 File a bug
@@ -68,7 +69,8 @@
68 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))69 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
69 |...Launchpad...) devmode demo site70 |...Launchpad...) devmode demo site
7071
71 >>> print extract_text(find_tag_by_id(browser.contents, 'globalheader'))72 >>> print extract_text(find_tags_by_class(
73 ... browser.contents, 'sitemessage')[0])
72 This is a demo site mmk. File a bug.74 This is a demo site mmk. File a bug.
73 >>> print browser.getLink(url="http://example.com").text75 >>> print browser.getLink(url="http://example.com").text
74 File a bug76 File a bug
@@ -86,16 +88,91 @@
86 >>> browser.open('http://launchpad.dev/ubuntu')88 >>> browser.open('http://launchpad.dev/ubuntu')
87 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))89 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
88 &bull;...Launchpad...) devmode90 &bull;...Launchpad...) devmode
89 >>> print find_tag_by_id(browser.contents, 'globalheader')91 >>> len(find_tags_by_class(browser.contents, 'sitemessage'))
90 None92 0
93
9194
92And for a non-3-0 page:95And for a non-3-0 page:
9396
94 >>> browser.open('http://launchpad.dev/')97 >>> browser.open('http://launchpad.dev/')
95 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))98 >>> print extract_text(find_tag_by_id(browser.contents, 'lp-version'))
96 |...Launchpad...) devmode99 |...Launchpad...) devmode
97 >>> print find_tag_by_id(browser.contents, 'globalheader')100 >>> len(find_tags_by_class(browser.contents, 'sitemessage'))
98 None101 0
102
103
104== Launchpad Edge ==
105
106Additionally, when a server is running as an edge server, the site-message
107is appended with a link to disable edge redirects.
108
109In addition to this prominent display on the root page, most pages will
110also include the disable-redirect link in the site_message - if the
111user is a member of the beta group and has not already disabled
112the redirects.
113
114 # Now setup an edge site-message config and re-check.
115 >>> edge_config_data = """
116 ... [launchpad]
117 ... site_message: This is a beta site.
118 ... is_edge: True
119 ... """
120 >>> config.push('edge_config_data', edge_config_data)
121 >>> beta_browser = setupBrowser(
122 ... auth='Basic beta-admin@launchpad.net:test')
123 >>> beta_browser.open('http://launchpad.dev/ubuntu')
124 >>> site_message = find_tags_by_class(
125 ... beta_browser.contents, 'sitemessage')[0]
126 >>> print extract_text(site_message)
127 This is a beta site. Disable edge redirect.
128 >>> print extract_text(site_message.find(
129 ... 'a', onclick="setBetaRedirect(false)"))
130 Disable edge redirect.
131
132The disable-redirect link will not appear for locationless pages, such
133as the login page, as the view does not inherit from LaunchpadView and
134so cannot support this functionality.
135
136 >>> beta_browser.open('http://launchpad.dev/+login')
137 >>> print extract_text(find_tags_by_class(
138 ... beta_browser.contents, 'sitemessage')[0])
139 This is a beta site.
140
141The disable-redirect link will not appear in the site_message when
142browsed by non-beta users.
143
144 >>> browser.open('http://launchpad.dev/ubuntu')
145 >>> print extract_text(find_tags_by_class(
146 ... browser.contents, 'sitemessage')[0])
147 This is a beta site.
148
149Similarly, once the redirection has been inhibited, the link changes to
150enable redirects..
151
152 # Workaround bug in mechanize where you cannot use the Cookie
153 # header with the CookieJar
154 >>> from mechanize._clientcookie import Cookie
155 >>> cookiejar = (
156 ... beta_browser.mech_browser._ua_handlers['_cookies'].cookiejar)
157 >>> cookiejar.set_cookie(
158 ... Cookie(
159 ... version=0, name='inhibit_beta_redirect', value='1', port=None,
160 ... port_specified=False, domain='.launchpad.dev',
161 ... domain_specified=True, domain_initial_dot=True, path='/',
162 ... path_specified=True, secure=False, expires=None,
163 ... discard=None, comment=None, comment_url=None, rest={}))
164 >>> beta_browser.open('http://launchpad.dev/ubuntu')
165 >>> site_message = find_tags_by_class(
166 ... beta_browser.contents, 'sitemessage')[0]
167 >>> print extract_text(site_message)
168 This is a beta site. Enable edge redirect.
169 >>> print extract_text(site_message.find(
170 ... 'a', onclick="setBetaRedirect(true)"))
171 Enable edge redirect.
172
173
174 # Remove the specific site-message config data before continuing.
175 >>> dummy = config.pop('edge_config_data')
99176
100177
101== Launchpad.net ==178== Launchpad.net ==
102179
=== modified file 'lib/canonical/launchpad/pagetests/basics/marketing.txt'
--- lib/canonical/launchpad/pagetests/basics/marketing.txt 2009-06-12 16:36:02 +0000
+++ lib/canonical/launchpad/pagetests/basics/marketing.txt 2009-09-07 13:15:05 +0000
@@ -153,9 +153,9 @@
153=== Bugs ===153=== Bugs ===
154154
155 >>> browser.open('http://bugs.launchpad.dev')155 >>> browser.open('http://bugs.launchpad.dev')
156 >>> tour_link = browser.getLink('Take a tour')156 >>> tour_link = browser.getLink('take a tour')
157 >>> print tour_link.url157 >>> print tour_link.url
158 http://launchpad.dev/+tour/bugs158 http://bugs.launchpad.dev/+tour
159 >>> tour_link.click()159 >>> tour_link.click()
160160
161161
162162
=== modified file 'lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt'
--- lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2009-08-27 07:05:16 +0000
+++ lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2009-09-16 22:57:42 +0000
@@ -67,8 +67,6 @@
67>>> check_redirect("/+index", host='feeds.launchpad.dev', status=301)67>>> check_redirect("/+index", host='feeds.launchpad.dev', status=301)
68>>> check("/+graphics")68>>> check("/+graphics")
6969
70>>> check("/translations/+about")
71
72>>> check("/+imports", host='translations.launchpad.dev')70>>> check("/+imports", host='translations.launchpad.dev')
73>>> check("/+imports/1/+index", auth=True, host='translations.launchpad.dev')71>>> check("/+imports/1/+index", auth=True, host='translations.launchpad.dev')
74>>> check_not_found("/+imports/foo", host='translations.launchpad.dev')72>>> check_not_found("/+imports/foo", host='translations.launchpad.dev')
@@ -202,10 +200,6 @@
202removing this, you must be completely sure that no supported Ubuntu release200removing this, you must be completely sure that no supported Ubuntu release
203is still pointing to this old URL (see bug #138090).201is still pointing to this old URL (see bug #138090).
204202
205>>> check_redirect("/ubuntu/hoary/+source/evolution/+translate",
206... host="launchpad.dev", status=301)
207>>> check("/ubuntu/hoary/+source/evolution/+translate",
208... host='translations.launchpad.dev' )
209>>> check_redirect("/ubuntu/hoary/+source/evolution/+translations", status=301)203>>> check_redirect("/ubuntu/hoary/+source/evolution/+translations", status=301)
210>>> check("/ubuntu/hoary/+source/evolution/+translations",204>>> check("/ubuntu/hoary/+source/evolution/+translations",
211... host='translations.launchpad.dev')205... host='translations.launchpad.dev')
@@ -408,7 +402,6 @@
408>>> check("/~name16/+edithomepage", auth=True)402>>> check("/~name16/+edithomepage", auth=True)
409>>> check("/~name16/+review", auth=True)403>>> check("/~name16/+review", auth=True)
410>>> check("/~name16/+portlet-emails")404>>> check("/~name16/+portlet-emails")
411>>> check("/~name16/+portlet-details")
412>>> check("/~name16/+portlet-team-assignedbugs")405>>> check("/~name16/+portlet-team-assignedbugs")
413>>> check("/~name16/+specworkload")406>>> check("/~name16/+specworkload")
414>>> check("/~name16/+imports", host='translations.launchpad.dev')407>>> check("/~name16/+imports", host='translations.launchpad.dev')
415408
=== modified file 'lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt'
--- lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2009-08-13 15:12:16 +0000
+++ lib/canonical/launchpad/pagetests/basics/page-request-summaries.txt 2009-09-16 22:57:42 +0000
@@ -14,4 +14,4 @@
14 >>> browser.open('http://launchpad.dev/~mark/')14 >>> browser.open('http://launchpad.dev/~mark/')
15 >>> print browser.contents15 >>> print browser.contents
16 <!DOCTYPE...16 <!DOCTYPE...
17 ...<!-- at least ... queries issued in ... seconds -->...17 ...<!--... At least ... queries issued in ... seconds ...-->...
1818
=== modified file 'lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt'
--- lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2009-07-23 00:09:16 +0000
+++ lib/canonical/launchpad/pagetests/basics/user-requested-oops.txt 2009-09-05 06:40:36 +0000
@@ -12,17 +12,18 @@
1212
13The OOPS id is put into the comment at the end of the document.13The OOPS id is put into the comment at the end of the document.
1414
15 >>> print browser.contents15 >>> (page, summary) = browser.contents.split('</html>')
16 <!DOCTYPE ...16 >>> print summary
17 ...
18 <!-- at least ... queries issued in ... seconds OOPS-... -->17 <!-- at least ... queries issued in ... seconds OOPS-... -->
19 <!-- Launchpad ... -->18 <!-- Launchpad ... -->
2019
21The ++oops++ can be anywhere in the traversal.20The ++oops++ can be anywhere in the traversal.
2221
23 >>> browser.open("http://launchpad.dev/gnome-terminal/++oops++/trunk")22 >>> browser.open("http://launchpad.dev/gnome-terminal/++oops++/trunk")
24 >>> print browser.contents23 >>> (page, summary) = browser.contents.split('</html>')
25 <!DOCTYPE ...24 >>> print summary
26 ...25 <!-- ...
27 <!-- at least ... queries issued in ... seconds OOPS-... -->26 At least ... queries issued in ... seconds OOPS-...
28 <!-- Launchpad ... -->27 <BLANKLINE>
28 Launchpad ...
29 -->
2930
=== removed directory 'lib/canonical/launchpad/pagetests/distroseries'
=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2009-07-06 14:31:45 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2009-09-09 20:08:36 +0000
@@ -47,7 +47,6 @@
4747
48 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')48 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
49 >>> print extract_text(main_content)49 >>> print extract_text(main_content)
50 Authorize application to access Launchpad on your behalf
51 The application identified as foobar123451432 wants to access Launchpad on50 The application identified as foobar123451432 wants to access Launchpad on
52 your behalf. What level of access do you want to grant?51 your behalf. What level of access do you want to grant?
53 ...52 ...
@@ -120,7 +119,6 @@
120 ... % urlencode(params_with_context))119 ... % urlencode(params_with_context))
121 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')120 >>> main_content = find_tag_by_id(browser.contents, 'maincontent')
122 >>> print extract_text(main_content)121 >>> print extract_text(main_content)
123 Authorize application to access Launchpad on your behalf
124 The application...wants to access things related to Mozilla Firefox...122 The application...wants to access things related to Mozilla Firefox...
125123
126A client other than a web browser may request a JSON representation of124A client other than a web browser may request a JSON representation of
127125
=== modified file 'lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt'
--- lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt 2009-07-01 13:16:44 +0000
+++ lib/canonical/launchpad/pagetests/oauth/managing-tokens.txt 2009-09-15 15:42:39 +0000
@@ -19,7 +19,7 @@
19 >>> my_browser = setupBrowser(auth='Basic salgado@ubuntu.com:zeca')19 >>> my_browser = setupBrowser(auth='Basic salgado@ubuntu.com:zeca')
20 >>> my_browser.open('http://launchpad.dev/~salgado/+oauth-tokens')20 >>> my_browser.open('http://launchpad.dev/~salgado/+oauth-tokens')
21 >>> print my_browser.title21 >>> print my_browser.title
22 Applications you authorized to access Launchpad22 +oauth-tokens : Guilherme Salgado
23 >>> main_content = find_tag_by_id(my_browser.contents, 'maincontent')23 >>> main_content = find_tag_by_id(my_browser.contents, 'maincontent')
24 >>> print extract_text(main_content)24 >>> print extract_text(main_content)
25 Authorized applications25 Authorized applications
@@ -41,7 +41,7 @@
41keys stored in hidden <input>s as well as the button to revoke the41keys stored in hidden <input>s as well as the button to revoke the
42authorization.42authorization.
4343
44 >>> li = main_content.find('li')44 >>> li = find_tag_by_id(main_content, 'tokens').find('li')
45 >>> for input in li.find('form').findAll('input'):45 >>> for input in li.find('form').findAll('input'):
46 ... print input['name'], input['value']46 ... print input['name'], input['value']
47 consumer_key foobar12345143247 consumer_key foobar123451432
@@ -71,7 +71,7 @@
7171
72 >>> my_browser.getControl('Revoke Authorization', index=1).click()72 >>> my_browser.getControl('Revoke Authorization', index=1).click()
73 >>> print my_browser.title73 >>> print my_browser.title
74 Applications you authorized to access Launchpad74 +oauth-tokens : Guilherme Salgado
75 >>> for message in get_feedback_messages(my_browser.contents):75 >>> for message in get_feedback_messages(my_browser.contents):
76 ... print message76 ... print message
77 Authorization revoked successfully.77 Authorization revoked successfully.
7878
=== modified file 'lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt'
--- lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt 2009-08-11 21:34:18 +0000
+++ lib/canonical/launchpad/pagetests/packaging/xx-ubuntu-pkging.txt 2009-09-09 23:16:08 +0000
@@ -124,8 +124,8 @@
124124
125 >>> user_browser.open('http://launchpad.dev/bzr/trunk/')125 >>> user_browser.open('http://launchpad.dev/bzr/trunk/')
126 >>> user_browser.getLink('Link to Ubuntu package').click()126 >>> user_browser.getLink('Link to Ubuntu package').click()
127 >>> user_browser.title127 >>> print user_browser.title
128 'Ubuntu source packaging'128 +ubuntupkg : Series trunk : Bazaar
129129
130 >>> user_browser.getControl(name='ubuntupkg').value = (130 >>> user_browser.getControl(name='ubuntupkg').value = (
131 ... "bzr<script>window.alert('XSS')</script>")131 ... "bzr<script>window.alert('XSS')</script>")
132132
=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt'
--- lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt 2006-11-29 13:24:27 +0000
+++ lib/canonical/launchpad/pagetests/standalone/xx-launchpad-integration.txt 2009-09-15 10:01:13 +0000
@@ -9,9 +9,4 @@
9 >>> 'Help and support' in anon_browser.contents9 >>> 'Help and support' in anon_browser.contents
10 True10 True
1111
12 >>> anon_browser.open(12
13 ... 'http://launchpad.dev/distros/ubuntu/hoary/+source/evolution/+translate'
14 ... )
15 >>> 'Help translate' in anon_browser.contents
16 True
17
1813
=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt'
--- lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt 2009-05-12 01:39:29 +0000
+++ lib/canonical/launchpad/pagetests/standalone/xx-login-without-preferredemail.txt 2009-09-09 23:16:08 +0000
@@ -55,11 +55,8 @@
5555
56 >>> path = "%s/+validateemail" % base_path56 >>> path = "%s/+validateemail" % base_path
57 >>> browser.open(path)57 >>> browser.open(path)
58 >>> print '\n' + browser.contents58 >>> print browser.title
59 <BLANKLINE>59 Confirm e-mail address
60 ...
61 ...Confirm e-mail address...martin.pitt@canonical.com...
62 ...
6360
64 >>> browser.getControl('Continue').click()61 >>> browser.getControl('Continue').click()
65 >>> browser.url62 >>> browser.url
@@ -77,4 +74,3 @@
77 >>> e = EmailAddressSet().getByEmail(to_addr)74 >>> e = EmailAddressSet().getByEmail(to_addr)
78 >>> e.status.title75 >>> e.status.title
79 'Preferred Email Address'76 'Preferred Email Address'
80
8177
=== modified file 'lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt'
--- lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2009-02-05 20:46:59 +0000
+++ lib/canonical/launchpad/pagetests/standalone/xx-opstats.txt 2009-09-15 10:55:13 +0000
@@ -46,6 +46,7 @@
46 500s: 046 500s: 0
47 503s: 047 503s: 0
48 5XXs: 048 5XXs: 0
49 5XXs_b: 0
49 6XXs: 050 6XXs: 0
50 http requests: 051 http requests: 0
51 requests: 052 requests: 0
@@ -108,6 +109,26 @@
108 http requests: 1109 http requests: 1
109 requests: 1110 requests: 1
110111
112We also have a special metric counting server errors returned to known
113web browsers (5XXs_b) - in the production environment we care more
114about errors returned to people than robots crawling obscure parts of
115the site.
116
117 >>> from textwrap import dedent
118 >>> output = http(dedent("""\
119 ... GET /error-test HTTP/1.1
120 ... Host: launchpad.dev
121 ... User-Agent: Mozilla/42.0
122 ... """))
123 >>> output.getStatus()
124 500
125 >>> report()
126 500s: 1
127 5XXs: 1
128 5XXs_b: 1
129 http requests: 1
130 requests: 1
131
111== Number of XML-RPC Faults ==132== Number of XML-RPC Faults ==
112133
113 >>> try:134 >>> try:
@@ -123,7 +144,6 @@
123144
124== Number of soft timeouts ==145== Number of soft timeouts ==
125146
126 >>> from textwrap import dedent
127 >>> from canonical.config import config147 >>> from canonical.config import config
128 >>> test_data = dedent("""148 >>> test_data = dedent("""
129 ... [database]149 ... [database]
130150
=== added file 'lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt'
--- lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/pagetests/webservice/xx-structuralsubscription.txt 2009-08-27 04:16:41 +0000
@@ -0,0 +1,151 @@
1= Structural Subscriptions =
2
3Structural subscriptions can be obtained from any target: a project,
4project series, project group, distribution, distribution series or
5distribution source package.
6
7 >>> login('admin@canonical.com')
8 >>> eric_db = factory.makePerson(name='eric')
9 >>> michael_db = factory.makePerson(name='michael')
10 >>> pythons_db = factory.makeTeam(name='pythons', owner=michael_db)
11 >>> pythons_db.addMember(eric_db, michael_db)
12
13 >>> fooix_db = factory.makeProduct(name='fooix', owner=eric_db)
14 >>> fooix01_db = fooix_db.newSeries(eric_db, '0.1', 'Series 0.1')
15 >>> logout()
16
17We can list the structural subscriptions on a target using the
18getSubscriptions named operation. There are none just yet.
19
20 >>> from lazr.restful.testing.webservice import (
21 ... pprint_collection, pprint_entry)
22 >>> subscriptions = webservice.named_get(
23 ... '/fooix', 'getSubscriptions').jsonBody()
24 >>> pprint_collection(subscriptions)
25 start: None
26 total_size: 0
27 ---
28
29Now Eric subscribes to Fooix's bug notifications.
30
31 >>> from canonical.launchpad.testing.pages import webservice_for_person
32 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
33 >>> eric_webservice = webservice_for_person(
34 ... eric_db, permission=OAuthPermission.WRITE_PRIVATE)
35
36 >>> print eric_webservice.named_post(
37 ... '/fooix', 'addBugSubscription')
38 HTTP/1.1 201 Created
39 ...
40 Location: http://.../fooix/+subscription/eric
41 ...
42
43 >>> subscriptions = webservice.named_get(
44 ... '/fooix', 'getSubscriptions').jsonBody()
45 >>> pprint_collection(subscriptions)
46 start: 0
47 total_size: 1
48 ---
49 date_created: u'...'
50 date_last_updated: u'...'
51 resource_type_link: u'http://.../#structural_subscription'
52 self_link: u'http://.../fooix/+subscription/eric'
53 subscribed_by_link: u'http://.../~eric'
54 subscriber_link: u'http://.../~eric'
55 target_link: u'http://.../fooix'
56 ---
57
58He can examine his subscription directly.
59
60 >>> pprint_entry(eric_webservice.named_get(
61 ... '/fooix', 'getSubscription',
62 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody())
63 date_created: u'...'
64 date_last_updated: u'...'
65 resource_type_link: u'http://.../#structural_subscription'
66 self_link: u'http://.../fooix/+subscription/eric'
67 subscribed_by_link: u'http://.../~eric'
68 subscriber_link: u'http://.../~eric'
69 target_link: u'http://.../fooix'
70
71If the subscription doesn't exist, None will be returned.
72
73 >>> print webservice.named_get(
74 ... '/fooix', 'getSubscription',
75 ... person=webservice.getAbsoluteUrl('/~michael')).jsonBody()
76 None
77
78Eric can remove his subscription through the webservice.
79
80 >>> print eric_webservice.named_post(
81 ... '/fooix', 'removeBugSubscription')
82 HTTP/1.1 200 Ok...
83
84 >>> print webservice.named_get(
85 ... '/fooix', 'getSubscription',
86 ... person=webservice.getAbsoluteUrl('/~eric')).jsonBody()
87 None
88
89Teams can be subscribed by passing in the team as an argument. Eric
90tries this.
91
92 >>> print eric_webservice.named_post(
93 ... '/fooix', 'addBugSubscription',
94 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
95 HTTP/1.1 401 Unauthorized
96 ...
97 UserCannotSubscribePerson: eric does not have permission to subscribe pythons.
98 <BLANKLINE>
99
100Oops, Eric isn't a team admin. Eric gets Michael to try, since he is an
101admin by virtue of his ownership.
102
103 >>> michael_webservice = webservice_for_person(
104 ... michael_db, permission=OAuthPermission.WRITE_PRIVATE)
105
106 >>> print michael_webservice.named_post(
107 ... '/fooix', 'addBugSubscription',
108 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
109 HTTP/1.1 201 Created
110 ...
111 Location: http://.../fooix/+subscription/pythons
112 ...
113
114 >>> subscriptions = webservice.named_get(
115 ... '/fooix', 'getSubscriptions').jsonBody()
116 >>> pprint_collection(subscriptions)
117 start: 0
118 total_size: 1
119 ---
120 date_created: u'...'
121 date_last_updated: u'...'
122 resource_type_link: u'http://.../#structural_subscription'
123 self_link: u'http://.../fooix/+subscription/pythons'
124 subscribed_by_link: u'http://.../~michael'
125 subscriber_link: u'http://.../~pythons'
126 target_link: u'http://.../fooix'
127 ---
128
129Eric can't unsubscribe the team either.
130
131 >>> print eric_webservice.named_post(
132 ... '/fooix', 'removeBugSubscription',
133 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
134 HTTP/1.1 401 Unauthorized
135 ...
136 UserCannotSubscribePerson: eric does not have permission to unsubscribe pythons.
137 <BLANKLINE>
138
139Michael can, though.
140
141 >>> print michael_webservice.named_post(
142 ... '/fooix', 'removeBugSubscription',
143 ... subscriber=webservice.getAbsoluteUrl('/~pythons'))
144 HTTP/1.1 200 Ok...
145
146 >>> subscriptions = webservice.named_get(
147 ... '/fooix', 'getSubscriptions').jsonBody()
148 >>> pprint_collection(subscriptions)
149 start: None
150 total_size: 0
151 ---
0152
=== modified file 'lib/canonical/launchpad/pagetitles.py'
--- lib/canonical/launchpad/pagetitles.py 2009-09-01 15:42:27 +0000
+++ lib/canonical/launchpad/pagetitles.py 2009-09-18 09:21:50 +0000
@@ -76,17 +76,6 @@
76 return self.text % context.displayname76 return self.text % context.displayname
7777
7878
79class FilteredTranslationsTitle(SubstitutionHelper):
80 """Return the formatted string with context's title and view's person."""
81 def __call__(self, context, view):
82 if view.person is not None:
83 person = view.person.displayname
84 else:
85 person = 'unknown'
86 return self.text % {'title' : context.title,
87 'person' : person }
88
89
90class ContextId(SubstitutionHelper):79class ContextId(SubstitutionHelper):
91 """Return the formatted string with context's id."""80 """Return the formatted string with context's id."""
92 def __call__(self, context, view):81 def __call__(self, context, view):
@@ -139,8 +128,6 @@
139128
140archive_edit_dependencies = ContextDisplayName('Edit dependencies for %s')129archive_edit_dependencies = ContextDisplayName('Edit dependencies for %s')
141130
142archive_index = ContextDisplayName('%s')
143
144archive_subscriber_edit = ContextDisplayName('Edit %s')131archive_subscriber_edit = ContextDisplayName('Edit %s')
145132
146archive_subscribers = ContextDisplayName('Manage access to %s')133archive_subscribers = ContextDisplayName('Manage access to %s')
@@ -178,8 +165,6 @@
178165
179bug_branch_add = LaunchbagBugID('Bug #%d - Add branch')166bug_branch_add = LaunchbagBugID('Bug #%d - Add branch')
180167
181bug_cve = LaunchbagBugID("Bug #%d - Add CVE reference")
182
183bug_edit = ContextBugId('Bug #%d - Edit')168bug_edit = ContextBugId('Bug #%d - Edit')
184169
185bug_edit_confirm = ContextBugId('Bug #%d - Edit confirmation')170bug_edit_confirm = ContextBugId('Bug #%d - Edit confirmation')
@@ -198,23 +183,14 @@
198183
199bug_nominate_for_series = ViewLabel()184bug_nominate_for_series = ViewLabel()
200185
201bug_removecve = LaunchbagBugID("Bug #%d - Remove CVE reference")
202
203bug_secrecy = ContextBugId('Bug #%d - Set visibility')186bug_secrecy = ContextBugId('Bug #%d - Set visibility')
204187
205bug_subscription = LaunchbagBugID('Bug #%d - Subscription options')188bug_subscription = LaunchbagBugID('Bug #%d - Subscription options')
206189
207bug_remove_question = LaunchbagBugID(
208 'Bug #%d - Convert this question back to a bug')
209
210bugbranch_delete = 'Delete bug branch link'190bugbranch_delete = 'Delete bug branch link'
211191
212bugbranch_edit = "Edit branch fix status"192bugbranch_edit = "Edit branch fix status"
213193
214def bugcomment_index(context, view):
215 """Return the page title for a bug comment."""
216 return "Bug #%d - Comment #%d" % (context.bug.id, view.comment.index)
217
218buglinktarget_linkbug = 'Link to bug report'194buglinktarget_linkbug = 'Link to bug report'
219195
220buglinktarget_unlinkbugs = 'Remove links to bug reports'196buglinktarget_unlinkbugs = 'Remove links to bug reports'
@@ -227,23 +203,11 @@
227 """Return the view's page heading."""203 """Return the view's page heading."""
228 return view.getSearchPageHeading()204 return view.getSearchPageHeading()
229205
230bug_listing_expirable = ContextTitle("Bugs that can expire in %s")
231
232def bugnomination_edit(context, view):206def bugnomination_edit(context, view):
233 """Return the title for the page to manage bug nominations."""207 """Return the title for the page to manage bug nominations."""
234 return 'Manage nomination for bug #%d in %s' % (208 return 'Manage nomination for bug #%d in %s' % (
235 context.bug.id, context.target.bugtargetdisplayname)209 context.bug.id, context.target.bugtargetdisplayname)
236210
237def bugwatch_editform(context, view):
238 """Return the title for the page to edit an external bug watch."""
239 return 'Bug #%d - Edit external bug watch (%s in %s)' % (
240 context.bug.id, context.remotebug, context.bugtracker.title)
241
242def bugwatch_comments(context, view):
243 """Return the title for a page of imported comments for a bug watch."""
244 return "Bug #%d - Comments imported from bug watch %s on %s" % (
245 context.bug.id, context.remotebug, context.bugtracker.title)
246
247def bugs_assigned(context, view):211def bugs_assigned(context, view):
248 """Return the page title for the bugs assigned to the logged-in user."""212 """Return the page title for the bugs assigned to the logged-in user."""
249 if view.user:213 if view.user:
@@ -293,27 +257,6 @@
293# bugtask_macros_buglisting contains only macros257# bugtask_macros_buglisting contains only macros
294# bugtasks_index is a redirect258# bugtasks_index is a redirect
295259
296bugtracker_edit = ContextTitle(
297 smartquote('Change details for "%s" bug tracker'))
298
299bugtracker_index = ContextTitle(smartquote('Bug tracker "%s"'))
300
301bugtrackers_add = 'Register an external bug tracker'
302
303bugtrackers_index = 'Bug trackers registered in Launchpad'
304
305build_buildlog = ContextTitle('Build log for %s')
306
307build_changes = ContextTitle('Changes in %s')
308
309build_index = ContextTitle('%s')
310
311build_retry = ContextTitle('Retry %s')
312
313build_rescore = ContextTitle('Rescore %s')
314
315builders_index = 'Launchpad build farm'
316
317calendar_index = ContextTitle('%s')260calendar_index = ContextTitle('%s')
318261
319calendar_event_addform = ContextTitle('Add event to %s')262calendar_event_addform = ContextTitle('Add event to %s')
@@ -357,8 +300,6 @@
357300
358codeofconduct_admin = 'Administer Codes of Conduct'301codeofconduct_admin = 'Administer Codes of Conduct'
359302
360codeofconduct_index = ContextTitle('%s')
361
362codeofconduct_list = 'Ubuntu Codes of Conduct'303codeofconduct_list = 'Ubuntu Codes of Conduct'
363304
364def contact_user(context, view):305def contact_user(context, view):
@@ -380,8 +321,6 @@
380321
381distributionmirror_index = ContextTitle('Mirror %s')322distributionmirror_index = ContextTitle('Mirror %s')
382323
383distribution_allpackages = ContextTitle('All packages in %s')
384
385distribution_archive_list = ContextTitle('%s Copy Archives')324distribution_archive_list = ContextTitle('%s Copy Archives')
386325
387distribution_upstream_bug_report = ContextTitle('Upstream Bug Report for %s')326distribution_upstream_bug_report = ContextTitle('Upstream Bug Report for %s')
@@ -392,8 +331,6 @@
392331
393distribution_mirrors = ContextTitle("Mirrors of %s")332distribution_mirrors = ContextTitle("Mirrors of %s")
394333
395distribution_series = ContextTitle("%s version history")
396
397distribution_translations = ContextDisplayName('Translating %s')334distribution_translations = ContextDisplayName('Translating %s')
398335
399distribution_translation_settings = ContextTitle(336distribution_translation_settings = ContextTitle(
@@ -405,8 +342,6 @@
405342
406distribution_builds = ContextTitle('%s builds')343distribution_builds = ContextTitle('%s builds')
407344
408distribution_ppa_list = ContextTitle('%s Personal Package Archives')
409
410distributionsourcepackage_bugs = ContextTitle('Bugs in %s')345distributionsourcepackage_bugs = ContextTitle('Bugs in %s')
411346
412distributionsourcepackage_index = ContextTitle('%s')347distributionsourcepackage_index = ContextTitle('%s')
@@ -414,11 +349,6 @@
414distributionsourcepackage_publishinghistory = ContextTitle(349distributionsourcepackage_publishinghistory = ContextTitle(
415 'Publishing history of %s')350 'Publishing history of %s')
416351
417structural_subscriptions_manage = ContextTitle(
418 'Bug subscriptions for %s')
419
420distributionsourcepackagerelease_index = ContextTitle('%s')
421
422distroarchseries_index = ContextTitle('%s in Launchpad')352distroarchseries_index = ContextTitle('%s in Launchpad')
423353
424distroarchseries_builds = ContextTitle('%s builds')354distroarchseries_builds = ContextTitle('%s builds')
@@ -434,33 +364,15 @@
434364
435distroseries_cvereport = ContextDisplayName('CVE report for %s')365distroseries_cvereport = ContextDisplayName('CVE report for %s')
436366
437def distroseries_index(context, view):
438 """Return the distribution and version page title."""
439 return '%s %s in Launchpad' % (
440 context.distribution.title, context.version)
441
442def distroseries_language_packs(context, view):367def distroseries_language_packs(context, view):
443 return view.page_title368 return view.page_title
444369
445distroseries_packaging = ContextDisplayName('Mapping packages to upstream '
446 'for %s')
447
448distroseries_search = ContextDisplayName('Search packages in %s')
449
450distroseries_translations = ContextTitle('Translations of %s in Launchpad')370distroseries_translations = ContextTitle('Translations of %s in Launchpad')
451371
452distroseries_builds = ContextTitle('%s builds')
453
454distroseries_queue = ContextTitle('Queue for %s')372distroseries_queue = ContextTitle('Queue for %s')
455373
456distroseriesbinarypackage_index = ContextTitle('%s')
457
458distroserieslanguage_index = ContextTitle('%s')
459
460distroseriessourcepackagerelease_index = ContextTitle('%s')374distroseriessourcepackagerelease_index = ContextTitle('%s')
461375
462edit_bug_supervisor = ContextTitle('Edit bug supervisor for %s')
463
464errorservice_config = 'Configure error log'376errorservice_config = 'Configure error log'
465377
466errorservice_entry = 'Error log entry'378errorservice_entry = 'Error log entry'
@@ -503,18 +415,12 @@
503415
504hassprints_sprints = ContextTitle("Events related to %s")416hassprints_sprints = ContextTitle("Events related to %s")
505417
506hastranslationimports_index = 'Translation import queue'
507
508hwdb_fingerprint_submissions = (418hwdb_fingerprint_submissions = (
509 "Hardware Database submissions for a fingerprint")419 "Hardware Database submissions for a fingerprint")
510420
511hwdb_submit_hardware_data = (421hwdb_submit_hardware_data = (
512 'Submit New Data to the Launchpad Hardware Database')422 'Submit New Data to the Launchpad Hardware Database')
513423
514language_index = ContextDisplayName("%s in Launchpad")
515
516languageset_index = 'Languages in Launchpad'
517
518# launchpad_debug doesn't need a title.424# launchpad_debug doesn't need a title.
519425
520def launchpad_addform(context, view):426def launchpad_addform(context, view):
@@ -538,30 +444,20 @@
538444
539# launchpad_js is standard javascript445# launchpad_js is standard javascript
540446
541launchpad_invalidbatchsize = "Invalid Batch Size"
542
543launchpad_legal = 'Launchpad legalese'447launchpad_legal = 'Launchpad legalese'
544448
545launchpad_login = 'Log in or register with Launchpad'449launchpad_login = 'Log in or register with Launchpad'
546450
547launchpad_notfound = 'Error: Page not found'
548
549launchpad_onezerostatus = 'One-Zero Page Template Status'451launchpad_onezerostatus = 'One-Zero Page Template Status'
550452
551launchpad_requestexpired = 'Error: Timeout'
552
553def launchpad_search(context, view):453def launchpad_search(context, view):
554 """Return the page title corresponding to the user's search."""454 """Return the page title corresponding to the user's search."""
555 return view.page_title455 return view.page_title
556456
557launchpad_translationunavailable = 'Translation page is not available'
558
559launchpad_unexpectedformdata = 'Error: Unexpected form data'457launchpad_unexpectedformdata = 'Error: Unexpected form data'
560458
561launchpad_librarianfailure = "Sorry, you can't do this right now"459launchpad_librarianfailure = "Sorry, you can't do this right now"
562460
563launchpad_readonlyfailure = "Sorry, you can't do this right now"
564
565# launchpad_widget_macros doesn't need a title.461# launchpad_widget_macros doesn't need a title.
566462
567launchpadstatisticset_index = 'Launchpad statistics'463launchpadstatisticset_index = 'Launchpad statistics'
@@ -579,31 +475,8 @@
579475
580loginservice_login = 'Launchpad Login Service'476loginservice_login = 'Launchpad Login Service'
581477
582logintoken_claimprofile = 'Claim Launchpad profile'
583
584logintoken_claimteam = 'Claim Launchpad team'
585
586# This page will always redirect the user to another page specific to the
587# login token in question, except when the token has been consumed already, in
588# which case the user will see the title.
589logintoken_index = 'You have already done this'
590
591logintoken_mergepeople = 'Merge Launchpad accounts'
592
593logintoken_newaccount = 'Create a new Launchpad account'
594
595logintoken_resetpassword = 'Forgotten your password?'
596
597loginservice_standalone_login = loginservice_login478loginservice_standalone_login = loginservice_login
598479
599logintoken_validateemail = 'Confirm e-mail address'
600
601logintoken_validategpg = 'Confirm OpenPGP key'
602
603logintoken_validatesignonlygpg = 'Confirm sign-only OpenPGP key'
604
605logintoken_validateteamemail = 'Confirm e-mail address'
606
607# main_template has the code to insert one of these titles.480# main_template has the code to insert one of these titles.
608481
609malone_about = 'About Launchpad Bugs'482malone_about = 'About Launchpad Bugs'
@@ -644,8 +517,6 @@
644 """Return the view's pagetitle."""517 """Return the view's pagetitle."""
645 return view.pagetitle518 return view.pagetitle
646519
647marketing_translations_about = "About Translations"
648
649marketing_translations_faq = "FAQs about Translations"520marketing_translations_faq = "FAQs about Translations"
650521
651mentoringofferset_success = "Successful mentorships over the past year."522mentoringofferset_success = "Successful mentorships over the past year."
@@ -658,8 +529,6 @@
658529
659milestone_add = ContextTitle('Add new milestone for %s')530milestone_add = ContextTitle('Add new milestone for %s')
660531
661milestone_index = ContextTitle('%s')
662
663milestone_edit = ContextTitle('Edit %s')532milestone_edit = ContextTitle('Edit %s')
664533
665milestone_delete = ContextTitle('Delete %s')534milestone_delete = ContextTitle('Delete %s')
@@ -689,8 +558,6 @@
689 """Return the page title to change the driver."""558 """Return the page title to change the driver."""
690 return view.page_title559 return view.page_title
691560
692object_milestones = ContextTitle(smartquote("%s's milestones"))
693
694# object_pots is a fragment.561# object_pots is a fragment.
695562
696object_translations = ContextDisplayName('Translation templates for %s')563object_translations = ContextDisplayName('Translation templates for %s')
@@ -723,8 +590,6 @@
723590
724openidrpconfigset_index = 'OpenID Relying Party Configurations'591openidrpconfigset_index = 'OpenID Relying Party Configurations'
725592
726official_bug_target_manage_tags = 'Manage Official Bug Tags'
727
728def package_bugs(context, view):593def package_bugs(context, view):
729 """Return the page title bug in a package."""594 """Return the page title bug in a package."""
730 return 'Bugs in %s' % context.name595 return 'Bugs in %s' % context.name
@@ -741,8 +606,6 @@
741606
742people_requestmerge_multiple = 'Merge Launchpad accounts'607people_requestmerge_multiple = 'Merge Launchpad accounts'
743608
744active_reviews = ContextDisplayName('Pending proposals for %s')
745
746person_archive_subscription = ContextDisplayName('%s')609person_archive_subscription = ContextDisplayName('%s')
747610
748person_archive_subscriptions = 'Private PPA access'611person_archive_subscriptions = 'Private PPA access'
@@ -750,33 +613,6 @@
750person_answer_contact_for = ContextDisplayName(613person_answer_contact_for = ContextDisplayName(
751 'Projects for which %s is an answer contact')614 'Projects for which %s is an answer contact')
752615
753person_changepassword = 'Change your password'
754
755person_claim = 'Claim account'
756
757person_claim_team = 'Claim team'
758
759person_deactivate_account = 'Deactivate your Launchpad account'
760
761person_codesofconduct = ContextDisplayName(
762 smartquote("%s's code of conduct signatures"))
763
764person_edit = ContextDisplayName(smartquote("%s's details"))
765
766person_editemails = ContextDisplayName(smartquote("%s's e-mail addresses"))
767
768person_editlocation = ContextDisplayName(smartquote("%s's usual location"))
769
770person_editpgpkeys = ContextDisplayName(smartquote("%s's OpenPGP keys"))
771
772person_editircnicknames = ContextDisplayName(smartquote("%s's IRC nicknames"))
773
774person_editjabberids = ContextDisplayName(smartquote("%s's Jabber IDs"))
775
776person_editsshkeys = ContextDisplayName(smartquote("%s's SSH keys"))
777
778person_editwikinames = ContextDisplayName(smartquote("%s's wiki names"))
779
780# person_foaf is an rdf file616# person_foaf is an rdf file
781617
782person_hwdb_submissions = ContextDisplayName(618person_hwdb_submissions = ContextDisplayName(
@@ -784,69 +620,27 @@
784620
785person_images = ContextDisplayName(smartquote("%s's hackergotchi and emblem"))621person_images = ContextDisplayName(smartquote("%s's hackergotchi and emblem"))
786622
787def person_index(context, view):
788 """Return the page title to the person index page."""
789 if context.is_valid_person_or_team:
790 return '%s in Launchpad' % context.displayname
791 else:
792 return "%s does not use Launchpad" % context.displayname
793
794person_karma = ContextDisplayName(smartquote("%s's karma in Launchpad"))623person_karma = ContextDisplayName(smartquote("%s's karma in Launchpad"))
795624
796person_maintained_packages = ContextDisplayName('Software maintained by %s')
797
798person_mentoringoffers = ContextTitle('Mentoring offered by %s')625person_mentoringoffers = ContextTitle('Mentoring offered by %s')
799626
800def person_mergeproposals(context, view):627def person_mergeproposals(context, view):
801 """Return the view's heading."""628 """Return the view's heading."""
802 return view.heading629 return view.heading
803630
804person_oauth_tokens = "Applications you authorized to access Launchpad"
805
806person_packagebugs = ContextDisplayName("%s's package bug reports")631person_packagebugs = ContextDisplayName("%s's package bug reports")
807632
808person_packagebugs_overview = person_packagebugs633person_packagebugs_overview = person_packagebugs
809634
810person_packagebugs_search = person_packagebugs635person_packagebugs_search = person_packagebugs
811636
812person_participation = ContextTitle("Team participation by %s")
813
814person_ppa_packages = ContextDisplayName('PPA packages related to %s')
815
816person_related_projects = ContextDisplayName('Projects related to %s')
817
818person_related_software = ContextDisplayName('Software related to %s')
819
820person_review = ContextDisplayName("Review %s")
821
822person_specfeedback = ContextDisplayName('Feature feedback requests for %s')637person_specfeedback = ContextDisplayName('Feature feedback requests for %s')
823638
824person_specworkload = ContextDisplayName('Blueprint workload for %s')639person_specworkload = ContextDisplayName('Blueprint workload for %s')
825640
826person_translations = ContextDisplayName('Translations related to %s')
827
828person_translations_relicensing = "Translations licensing"
829
830person_translations_to_review = ContextDisplayName(641person_translations_to_review = ContextDisplayName(
831 'Translations for review by %s')642 'Translations for review by %s')
832643
833person_teamhierarchy = ContextDisplayName('Team hierarchy for %s')
834
835person_uploaded_packages = ContextDisplayName('Software uploaded by %s')
836
837person_vouchers = ContextDisplayName(
838 'Commercial subscription vouchers for %s')
839
840pofile_filter = FilteredTranslationsTitle(
841 smartquote('Translations by %(person)s in "%(title)s"'))
842
843pofile_index = ContextTitle(smartquote('Translation overview for "%s"'))
844
845def pofile_translate(context, view):
846 """Return the page to translate a template into a language."""
847 return 'Translating %s into %s' % (
848 context.potemplate.displayname, context.language.englishname)
849
850# portlet_* are portlets644# portlet_* are portlets
851645
852poll_edit = ContextTitle(smartquote('Edit poll "%s"'))646poll_edit = ContextTitle(smartquote('Edit poll "%s"'))
@@ -869,17 +663,12 @@
869663
870poll_vote_simple = ContextTitle(smartquote('Vote in poll "%s"'))664poll_vote_simple = ContextTitle(smartquote('Vote in poll "%s"'))
871665
872potemplate_index = ContextTitle(smartquote('Translation status for "%s"'))
873
874product_admin = ContextTitle('Administer %s in Launchpad')666product_admin = ContextTitle('Administer %s in Launchpad')
875667
876product_bugs = ContextDisplayName('Bugs in %s')668product_bugs = ContextDisplayName('Bugs in %s')
877669
878product_code_index = ContextDisplayName("Bazaar branches of %s")670product_code_index = ContextDisplayName("Bazaar branches of %s")
879671
880product_distros = ContextDisplayName(
881 '%s packages: Comparison of distributions')
882
883product_cvereport = ContextTitle('CVE reports for %s')672product_cvereport = ContextTitle('CVE reports for %s')
884673
885product_edit = 'Change project details'674product_edit = 'Change project details'
@@ -893,57 +682,25 @@
893 """Return the view's heading."""682 """Return the view's heading."""
894 return view.heading683 return view.heading
895684
896def product_new(context, view):
897 """Return the view's heading."""
898 return view.heading
899
900product_new_guided = 'Before you register your project...'685product_new_guided = 'Before you register your project...'
901686
902product_packages = ContextDisplayName('%s packages in Launchpad')
903
904product_purchase_subscription = ContextDisplayName(687product_purchase_subscription = ContextDisplayName(
905 'Purchase Subscription for %s')688 'Purchase Subscription for %s')
906689
907product_review_license = ContextTitle('Review %s')690product_review_license = ContextTitle('Review %s')
908691
909product_series = ContextDisplayName('%s timeline')
910
911product_timeline = ContextTitle('Timeline Diagram for %s')692product_timeline = ContextTitle('Timeline Diagram for %s')
912693
913product_translations = ContextTitle('Translations of %s in Launchpad')694product_translations = ContextTitle('Translations of %s in Launchpad')
914695
915productrelease_add = ContextDisplayName('Publish the release of %s')
916
917productrelease_add_from_series = productrelease_add
918
919productrelease_delete = ContextTitle('Delete %s in Launchpad')
920
921productrelease_file_add = ContextDisplayName('Add a file to %s')
922
923productrelease_admin = ContextTitle('Administer %s in Launchpad')696productrelease_admin = ContextTitle('Administer %s in Launchpad')
924697
925productrelease_edit = ContextDisplayName('Edit details of %s in Launchpad')
926
927productrelease_index = ContextDisplayName('%s in Launchpad')698productrelease_index = ContextDisplayName('%s in Launchpad')
928699
929products_review_licenses = 'Review projects'
930
931productserieslanguage_index = ContextTitle('%s')
932
933productseries_index = ContextTitle('%s')
934
935productseries_packaging = ContextDisplayName(
936 'Packaging of %s in distributions')
937
938productseries_translations = ContextTitle('Translations overview for %s')700productseries_translations = ContextTitle('Translations overview for %s')
939701
940productseries_translations_settings = 'Settings for translations'702productseries_translations_settings = 'Settings for translations'
941703
942productseries_translations_bzr_import = (
943 'Request translations import from Bazaar branch')
944
945project_add = 'Register a project group with Launchpad'
946
947project_index = ContextTitle('%s in Launchpad')704project_index = ContextTitle('%s in Launchpad')
948705
949project_bugs = ContextTitle('Bugs in %s')706project_bugs = ContextTitle('Bugs in %s')
@@ -1049,22 +806,10 @@
1049806
1050sourcepackage_builds = ContextTitle('Builds for %s')807sourcepackage_builds = ContextTitle('Builds for %s')
1051808
1052sourcepackage_translate = ContextTitle('Help translate %s')
1053
1054sourcepackage_changelog = 'Source package changelog'809sourcepackage_changelog = 'Source package changelog'
1055810
1056sourcepackage_filebug = ContextTitle("Report a bug about %s")811sourcepackage_filebug = ContextTitle("Report a bug about %s")
1057812
1058sourcepackage_gethelp = ContextTitle('Help and support options for %s')
1059
1060sourcepackage_packaging = ContextTitle('%s upstream links')
1061
1062def sourcepackage_index(context, view):
1063 """Return the page title for a source package in a distroseries."""
1064 return '%s source packages' % context.distroseries.title
1065
1066sourcepackage_translate = ContextTitle('Help translate %s')
1067
1068sourcepackagenames_index = 'Source package name set'813sourcepackagenames_index = 'Source package name set'
1069814
1070sourcepackagerelease_index = ContextTitle('Source package %s')815sourcepackagerelease_index = ContextTitle('Source package %s')
@@ -1135,8 +880,6 @@
1135 """Return the page title for subscribing to a specification."""880 """Return the page title for subscribing to a specification."""
1136 return "Subscription of %s" % context.person.displayname881 return "Subscription of %s" % context.person.displayname
1137882
1138specificationtarget_documentation = ContextTitle('Documentation for %s')
1139
1140specificationtarget_index = ContextTitle('Blueprint listing for %s')883specificationtarget_index = ContextTitle('Blueprint listing for %s')
1141884
1142def specificationtarget_specs(context, view):885def specificationtarget_specs(context, view):
@@ -1173,48 +916,8 @@
1173916
1174standardshipitrequest_edit = 'Edit standard option'917standardshipitrequest_edit = 'Edit standard option'
1175918
1176team_addmember = ContextBrowsername('Add members to %s')
1177
1178team_add_my_teams = 'Propose/add one of your teams to another one'
1179
1180team_editproposed = ContextBrowsername('Proposed members of %s')
1181
1182team_index = ContextBrowsername('%s in Launchpad')919team_index = ContextBrowsername('%s in Launchpad')
1183920
1184team_invitations = ContextBrowsername("Invitations sent to %s")
1185
1186team_join = ContextBrowsername('Join %s')
1187
1188team_leave = ContextBrowsername('Leave %s')
1189
1190team_mailinglist = 'Configure mailing list'
1191
1192team_mailinglist_moderate = 'Moderate mailing list'
1193
1194team_mailinglist_subscribers = ContextBrowsername(
1195 'Mailing list subscribers for the %s team')
1196
1197team_map = ContextBrowsername('Map of %s participants')
1198
1199team_members = ContextBrowsername(smartquote('"%s" members'))
1200
1201team_mugshots = ContextBrowsername(smartquote('Mugshots in the "%s" team'))
1202
1203def teammembership_index(context, view):
1204 """Return the page title to the persons status in a team."""
1205 return smartquote("%s's membership status in %s") % (
1206 context.person.displayname, context.team.displayname)
1207
1208def teammembership_invitation(context, view):
1209 """Return the page title to invite a person to become a team member."""
1210 return "Make %s a member of %s" % (
1211 context.person.displayname, context.team.displayname)
1212
1213def teammembership_self_renewal(context, view):
1214 """Return the page title renew membership in a team."""
1215 return "Renew membership of %s in %s" % (
1216 context.person.displayname, context.team.displayname)
1217
1218team_mentoringoffers = ContextTitle('Mentoring available for newcomers to %s')921team_mentoringoffers = ContextTitle('Mentoring available for newcomers to %s')
1219922
1220team_newpoll = ContextTitle('New poll for team %s')923team_newpoll = ContextTitle('New poll for team %s')
@@ -1235,30 +938,6 @@
1235938
1236token_authorized = 'Almost finished ...'939token_authorized = 'Almost finished ...'
1237940
1238translationgroup_index = ContextTitle(
1239 smartquote('"%s" Launchpad translation group'))
1240
1241translationgroup_appoint = ContextTitle(
1242 smartquote('Appoint a new translator to "%s"'))
1243
1244translationgroup_edit = ContextTitle(smartquote(
1245 'Edit "%s" translation group details'))
1246
1247translationgroup_reassignment = ContextTitle(smartquote(
1248 'Change the owner of "%s" translation group'))
1249
1250translationgroups_index = 'Launchpad translation groups'
1251
1252translationimportqueueentry_index = 'Translation import queue entry'941translationimportqueueentry_index = 'Translation import queue entry'
1253942
1254translationimportqueue_index = 'Translation import queue'
1255
1256translationimportqueue_blocked = 'Translation import queue - Blocked'
1257
1258def translationmessage_translate(context, view):
1259 """Return the page to translate a template into a language per message."""
1260 return 'Translating %s into %s' % (
1261 context.pofile.potemplate.displayname,
1262 context.pofile.language.englishname)
1263
1264unauthorized = 'Error: Not authorized'943unauthorized = 'Error: Not authorized'
1265944
=== modified file 'lib/canonical/launchpad/scripts/hardware-1_0.rng'
--- lib/canonical/launchpad/scripts/hardware-1_0.rng 2009-02-24 17:43:37 +0000
+++ lib/canonical/launchpad/scripts/hardware-1_0.rng 2009-09-14 09:16:45 +0000
@@ -98,40 +98,64 @@
98 </element>98 </element>
99 </zeroOrMore>99 </zeroOrMore>
100 </element>100 </element>
101 <optional>
102 <element name="kernel-release">
103 <attribute name="value">
104 <text/>
105 </attribute>
106 </element>
107 </optional>
101 </interleave>108 </interleave>
102 </define>109 </define>
103110
104 <define name="hardwareSection">111 <define name="hardwareSection">
105 <interleave>112 <interleave>
106 <element name="hal">113 <choice>
107 <attribute name="version">114 <element name="hal">
108 <text/>115 <attribute name="version">
109 </attribute>116 <text/>
110 <oneOrMore>117 </attribute>
111 <element name="device">118 <oneOrMore>
112 <attribute name="id">119 <element name="device">
113 <data type="integer">120 <attribute name="id">
114 <except>121 <data type="integer">
115 <value/>122 <except>
116 </except>123 <value/>
117 </data>124 </except>
118 </attribute>125 </data>
119 <attribute name="udi">126 </attribute>
120 <text/>127 <attribute name="udi">
121 </attribute>128 <text/>
122 <optional>129 </attribute>
123 <attribute name="parent">130 <optional>
124 <data type="integer"/>131 <attribute name="parent">
125 </attribute>132 <data type="integer"/>
126 </optional>133 </attribute>
127 <!-- XXX: Abel Deuring 2007-12-07:134 </optional>
128 specify a set of required properties? -->135 <!-- XXX: Abel Deuring 2007-12-07:
129 <oneOrMore>136 specify a set of required properties? -->
130 <ref name="property"/>137 <oneOrMore>
131 </oneOrMore>138 <ref name="property"/>
132 </element>139 </oneOrMore>
133 </oneOrMore>140 </element>
134 </element>141 </oneOrMore>
142 </element>
143 <group>
144 <interleave>
145 <element name="udev">
146 <text/>
147 </element>
148 <element name="dmi">
149 <text/>
150 </element>
151 <element name="sysfs-attributes">
152 <zeroOrMore>
153 <text/>
154 </zeroOrMore>
155 </element>
156 </interleave>
157 </group>
158 </choice>
135 <element name="processors">159 <element name="processors">
136 <oneOrMore>160 <oneOrMore>
137 <element name="processor">161 <element name="processor">
@@ -156,11 +180,7 @@
156 <zeroOrMore>180 <zeroOrMore>
157 <element name="alias">181 <element name="alias">
158 <attribute name="target">182 <attribute name="target">
159 <data type="integer">183 <text/>
160 <except>
161 <value/>
162 </except>
163 </data>
164 </attribute>184 </attribute>
165 <interleave>185 <interleave>
166 <element name="vendor">186 <element name="vendor">
@@ -292,11 +312,7 @@
292 <zeroOrMore>312 <zeroOrMore>
293 <element name="target">313 <element name="target">
294 <attribute name="id">314 <attribute name="id">
295 <data type="integer">315 <text/>
296 <except>
297 <value/>
298 </except>
299 </data>
300 </attribute>316 </attribute>
301 <interleave>317 <interleave>
302 <zeroOrMore>318 <zeroOrMore>
303319
=== modified file 'lib/canonical/launchpad/scripts/hwdbsubmissions.py'
--- lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/scripts/hwdbsubmissions.py 2009-09-15 17:05:32 +0000
@@ -16,7 +16,10 @@
1616
17import bz217import bz2
18from cStringIO import StringIO18from cStringIO import StringIO
19import cElementTree as etree19try:
20 import xml.etree.cElementTree as etree
21except ImportError:
22 import cElementTree as etree
20from datetime import datetime, timedelta23from datetime import datetime, timedelta
21from logging import getLogger24from logging import getLogger
22import os25import os
@@ -437,16 +440,111 @@
437 aliases.append(alias)440 aliases.append(alias)
438 return aliases441 return aliases
439442
440 _parse_hardware_section = {443 def _parseUdev(self, udev_node):
441 'hal': _parseHAL,444 """Parse the <udev> node.
442 'processors': _parseProcessors,445
443 'aliases': _parseAliases}446 :return: A list of dictionaries, where each dictionary
447 describes a udev device.
448
449 The <udev> node contains the output produced by
450 "udevadm info --export-db". Each entry of the dictionaries
451 represents the data of the key:value pairs as they appear
452 in this data. The value of d['S'] is a list of strings,
453 the value s['E'] is a dictionary containing the key=value
454 pairs of the "E:" lines.
455 """
456 # We get the plain text as produced by "udevadm info --export-db"
457 # This data looks like:
458 #
459 # P: /devices/LNXSYSTM:00
460 # E: UDEV_LOG=3
461 # E: DEVPATH=/devices/LNXSYSTM:00
462 # E: MODALIAS=acpi:LNXSYSTM:
463 #
464 # P: /devices/LNXSYSTM:00/ACPI_CPU:00
465 # E: UDEV_LOG=3
466 # E: DEVPATH=/devices/LNXSYSTM:00/ACPI_CPU:00
467 # E: DRIVER=processor
468 # E: MODALIAS=acpi:ACPI_CPU:
469 #
470 # Data for different devices is separated by empty lines.
471 # Each line for a device consists of key:value pairs.
472 # The following keys are defined:
473 #
474 # A: udev_device_get_num_fake_partitions()
475 # E: udev_device_get_properties_list_entry()
476 # L: the device link priority (udev_device_get_devlink_priority())
477 # N: the device node file name (udev_device_get_devnode())
478 # P: the device path (udev_device_get_devpath())
479 # R: udev_device_get_ignore_remove()
480 # S: udev_get_dev_path()
481 # W: udev_device_get_watch_handle()
482 #
483 # The key P is always present; the keys A, L, N, R, W appear at
484 # most once per device; the keys E and S may appear more than
485 # once.
486 # The values of the E records have the format "key=value"
487 #
488 # See also the libudev reference manual:
489 # http://www.kernel.org/pub/linux/utils/kernel/hotplug/libudev/
490 # and the udev file udevadm-info.c, function print_record()
491
492 udev_data = udev_node.text.split('\n')
493 devices = []
494 device = None
495 line_number = 0
496
497 for line_number, line in enumerate(udev_data):
498 if len(line) == 0:
499 device = None
500 continue
501 record = line.split(':', 1)
502 if len(record) != 2:
503 self._logError(
504 'Line %i in <udev>: No valid key:value data: %r'
505 % (line_number, line),
506 self.submission_key)
507 return None
508
509 key, value = record
510 if device is None:
511 device = {
512 'E': {},
513 'S': [],
514 }
515 devices.append(device)
516 # Some attribute lines have a space character after the
517 # ':', others don't have it (see udevadm-info.c).
518 value = value.lstrip()
519
520 if key == 'E':
521 property_data = value.split('=', 1)
522 if len(property_data) != 2:
523 self._logError(
524 'Line %i in <udev>: Property without valid key=value '
525 'data: %r' % (line_number, line),
526 self.submission_key)
527 return None
528 property_key, property_value = property_data
529 device['E'][property_key] = property_value
530 elif key == 'S':
531 device['S'].append(value)
532 else:
533 if key in device:
534 self._logWarning(
535 'Line %i in <udev>: Duplicate attribute key: %r'
536 % (line_number, line),
537 self.submission_key)
538 device[key] = value
539 return devices
444540
445 def _setHardwareSectionParsers(self):541 def _setHardwareSectionParsers(self):
446 self._parse_hardware_section = {542 self._parse_hardware_section = {
447 'hal': self._parseHAL,543 'hal': self._parseHAL,
448 'processors': self._parseProcessors,544 'processors': self._parseProcessors,
449 'aliases': self._parseAliases}545 'aliases': self._parseAliases,
546 'udev': self._parseUdev,
547 }
450548
451 def _parseHardware(self, hardware_node):549 def _parseHardware(self, hardware_node):
452 """Parse the <hardware> part of a submission.550 """Parse the <hardware> part of a submission.
453551
=== modified file 'lib/canonical/launchpad/scripts/sftracker.py'
--- lib/canonical/launchpad/scripts/sftracker.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/scripts/sftracker.py 2009-09-04 10:43:39 +0000
@@ -34,7 +34,7 @@
3434
35# use cElementTree if it is available ...35# use cElementTree if it is available ...
36try:36try:
37 import xml.elementtree.cElementTree as ET37 import xml.etree.cElementTree as ET
38except ImportError:38except ImportError:
39 try:39 try:
40 import cElementTree as ET40 import cElementTree as ET
4141
=== added file 'lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml'
--- lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/scripts/tests/hardwaretest-udev.xml 2009-09-14 09:16:45 +0000
@@ -0,0 +1,453 @@
1<?xml version="1.0" ?>
2<system version="1.0">
3
4 <!-- summary: generic information about the submission -->
5 <summary>
6
7 <!-- live_cd: Was this submission made on a system running an Ubuntu Live
8 CD or on a regular Ubuntu/Linux installation?
9 -->
10 <live_cd value="False"/>
11
12 <!-- system_id: A hash of the "system identifier". This value is intended
13 to identify the tested computer model; the value should
14 be derived from the properties
15 system.product, system.vendor of the HAL UDI
16 /org/freedesktop/Hal/devices/computer.
17 -->
18 <system_id value="f982bb1ab536469cebfd6eaadcea0ffc"/>
19
20 <!-- distribution, distroseries: These values are retrieved from
21 /etc/lsb-release, parameters DISTRIB_ID and DISTRIB_RELEASE.
22 -->
23 <distribution value="Ubuntu"/>
24 <distroseries value="7.04"/>
25
26 <!-- architecture: The processor architecture of the operating system.
27 -->
28 <architecture value="amd64"/>
29
30 <!-- private: If False, this submission is publicly accessible from
31 Launchpad, else it is only accesible by the submitter, by
32 Launchpad administrators and by scripts running with
33 administrator rights. Submissions marked "private" should
34 only be used to gather statistical data.
35 -->
36 <private value="False"/>
37
38 <!-- contactable: If True, the owner agrees to be contacted by other
39 persons about devices which appear in his submission.
40 Example of a use case: Developers can ask device owners
41 to perform tests.
42 -->
43 <contactable value="False"/>
44
45 <!-- date_created: Date and time (UTC) of the submission.
46 -->
47 <date_created value="2007-09-28T16:09:20.126842"/>
48
49 <!-- client: The name and version of the program that created the
50 submission data.
51 -->
52 <client name="hwtest" version="0.9">
53
54 <!-- plugin: name and version of a plugin used by the client.
55 This tag may appear more than once.
56 -->
57 <plugin name="architecture_info" version="1.1"/>
58 <plugin name="find_network_controllers" version="2.34"/>
59 <plugin name="internet_ping" version="1.1"/>
60 <plugin name="harddisk_speed" version="0.7"/>
61 </client>
62
63 <!-- The kernel name and version, as shown by "uname -r"
64 -->
65 <kernel-release value="2.6.28-14-generic"/>
66 </summary>
67
68 <!-- hardware: data about the hardware the submission was made on.
69 -->
70 <hardware>
71
72 <!-- udev: The output of running "udevadm info - -export-db" -->
73
74 <udev>P: /devices/LNXSYSTM:00
75E: UDEV_LOG=3
76E: DEVPATH=/devices/LNXSYSTM:00
77E: MODALIAS=acpi:LNXSYSTM:
78
79P: /devices/pci0000:00/0000:00:1a.0
80E: UDEV_LOG=3
81E: DEVPATH=/devices/pci0000:00/0000:00:1a.0
82E: DRIVER=uhci_hcd
83E: PCI_CLASS=C0300
84E: PCI_ID=8086:2834
85E: PCI_SUBSYS_ID=17AA:20AA
86E: PCI_SLOT_NAME=0000:00:1a.0
87E: MODALIAS=pci:v00008086d00002834sv000017AAsd000020AAbc0Csc03i00
88
89P: /devices/pci0000:00/0000:00:1a.0/usb3
90N: bus/usb/003/001
91S: char/189:256
92E: UDEV_LOG=3
93E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3
94E: MAJOR=189
95E: MINOR=256
96E: DEVTYPE=usb_device
97E: DRIVER=usb
98E: DEVICE=/proc/bus/usb/003/001
99E: PRODUCT=1d6b/1/206
100E: TYPE=9/0/0
101E: BUSNUM=003
102E: DEVNUM=001
103E: DEVNAME=/dev/bus/usb/003/001
104E: DEVLINKS=/dev/char/189:256
105
106P: /devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0
107E: UDEV_LOG=3
108E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0
109E: DEVTYPE=usb_interface
110E: DRIVER=hub
111E: DEVICE=/proc/bus/usb/003/001
112E: PRODUCT=1d6b/1/206
113E: TYPE=9/0/0
114E: INTERFACE=9/0/0
115E: MODALIAS=usb:v1D6Bp0001d0206dc09dsc00dp00ic09isc00ip00
116
117P: /devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81
118N: usbdev3.1_ep81
119S: char/252:4
120E: UDEV_LOG=3
121E: DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81
122E: MAJOR=252
123E: MINOR=4
124E: DEVNAME=/dev/usbdev3.1_ep81
125E: DEVLINKS=/dev/char/252:4
126
127P: /devices/pci0000:00/0000:00:1f.1
128E: UDEV_LOG=3
129E: DEVPATH=/devices/pci0000:00/0000:00:1f.1
130E: DRIVER=ata_piix
131E: PCI_CLASS=1018A
132E: PCI_ID=8086:2850
133E: PCI_SUBSYS_ID=17AA:20A6
134E: PCI_SLOT_NAME=0000:00:1f.1
135E: MODALIAS=pci:v00008086d00002850sv000017AAsd000020A6bc01sc01i8a
136
137P: /devices/pci0000:00/0000:00:1f.1/host3
138E: UDEV_LOG=3
139E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3
140E: DEVTYPE=scsi_host
141
142P: /devices/pci0000:00/0000:00:1f.1/host3/scsi_host/host3
143E: UDEV_LOG=3
144E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/scsi_host/host3
145
146P: /devices/pci0000:00/0000:00:1f.1/host3/target3:0:0
147E: UDEV_LOG=3
148E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0
149E: DEVTYPE=scsi_target
150
151P: /devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0
152E: UDEV_LOG=3
153E: DEVPATH=/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0
154E: DEVTYPE=scsi_device
155E: DRIVER=sr
156E: MODALIAS=scsi:t-0x05
157</udev>
158
159 <!-- The content of publicly accessible files in /sys/class/dmi/id/
160 format: filename:content
161 as for example generated by "grep -r . /sys/class/dmi/id/"
162 -->
163
164 <dmi>/sys/class/dmi/id/bios_vendor:LENOVO
165/sys/class/dmi/id/bios_version:7LETB9WW (2.19 )
166/sys/class/dmi/id/bios_date:06/06/2008
167/sys/class/dmi/id/sys_vendor:LENOVO
168/sys/class/dmi/id/product_name:6457BAG
169/sys/class/dmi/id/product_version:ThinkPad T61
170/sys/class/dmi/id/board_vendor:LENOVO
171/sys/class/dmi/id/board_name:6457BAG
172/sys/class/dmi/id/board_version:Not Available
173/sys/class/dmi/id/chassis_vendor:LENOVO
174/sys/class/dmi/id/chassis_type:10
175/sys/class/dmi/id/chassis_version:Not Available
176/sys/class/dmi/id/chassis_asset_tag:No Asset Information
177/sys/class/dmi/id/modalias:dmi:bvnLENOVO:bvr7LETB9WW(2.19)
178</dmi>
179
180 <!-- Additional data for SCSI devices: vendor, model, type
181
182 For each udev node which has DEVTYPE=scsi_device, we need
183 the content of the sysfs files vendor, model, type. The data
184 is stored in the same format as the DMI data:
185 /path/to/file:filecontent
186 -->
187 <sysfs-attributes>/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/vendor:HL-DT-ST
188/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/model:DVDRAM GSA-4083N
189/sys/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0/type:5
190 </sysfs-attributes>
191
192 <!-- processors: Data about processors installed in a system.
193 The data is retrieved from /proc/cpuinfo.
194 -->
195 <processors>
196
197 <!-- processor: Data from /proc/cpuinfo about a single processor.
198 -->
199 <processor id="123" name="0">
200
201 <!-- property: The data of one line of /proc/cpuinfo.
202 attribute name: The name of the property
203 (the text left of the ':' in a line of /proc/cpuinfo)
204 attribute type: A Python type appropriate for the value.
205 -->
206 <property name="wp" type="bool">
207 True
208 </property>
209 <property name="flags" type="list">
210 <value type="str">
211 fpu
212 </value>
213 <value type="str">
214 vme
215 </value>
216 <value type="str">
217 de
218 </value>
219 </property>
220 <property name="cpu_mhz" type="float">
221 1000.0
222 </property>
223 </processor>
224 </processors>
225
226 <!-- aliases: optional data provided by the user:
227 The name of a peripheral, PCI card etc as shown by a label on
228 the device. OEM devices are often sold under different names
229 by different vendors; having a set of alias names for a device
230 allows users of the HWDB to search for information by these
231 "marketing names".
232 -->
233 <aliases>
234 <!-- alias: The "label name" of a device or system.
235 attribute target: The sysfs path of a device as given
236 in <udev>.
237 -->
238 <alias target="/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0">
239
240 <!-- vendor: The vendor name shown on the device label.
241 -->
242 <vendor>Medion</vendor>
243
244 <!-- model: The model name of shown on the label.
245 -->
246 <model>QuickPrint 9876</model>
247 </alias>
248 </aliases>
249 </hardware>
250
251 <!-- software: Data about the software installed on the system.
252 -->
253 <software>
254
255 <!-- lsbrelease: The data from /etc/lsb-release.
256 -->
257 <lsbrelease>
258
259 <!-- property: the data from one line of /etc/lsb-release.
260 attribute type: A Python type appropriate for this
261 property (str).
262 -->
263 <property name="release" type="str">
264 7.04
265 </property>
266 <property name="codename" type="str">
267 feisty
268 </property>
269 <property name="distributor-id" type="str">
270 Ubuntu
271 </property>
272 <property name="description" type="str">
273 Ubuntu 7.04
274 </property>
275 <property name="dict_example" type="dict">
276 <value name="a" type="str">value for key a</value>
277 <value name="b" type="int">1234</value>
278 </property>
279 </lsbrelease>
280
281 <!-- packages: Data about the installed software packages.
282 -->
283 <packages>
284
285 <!-- package: Data about a single package.
286 The <property> sub-tags contain the DEB properties
287 "name", "priority", "section", "source", "version",
288 "installed_size", "size", "summary".
289
290 XXX Abel Deuring 2007-12-12: What about submissions
291 from RPM-based Linux versions? (And "exotic" variants
292 like Gentoo?)
293 -->
294 <package name="metacity" id="200">
295 <property name="installed_size" type="int">
296 868352
297 </property>
298 <property name="section" type="str">
299 x11
300 </property>
301 <property name="summary" type="str">
302 A lightweight GTK2 based Window Manager
303 </property>
304 <property name="priority" type="str">
305 optional
306 </property>
307 <property name="source" type="str">
308 metacity
309 </property>
310 <property name="version" type="str">
311 1:2.18.2-0ubuntu1.1
312 </property>
313 <property name="size" type="int">
314 429128
315 </property>
316 </package>
317 </packages>
318 <!-- Information extracted from Xorg.0.log.
319 HAL does not provide any information about Xorg drivers, so
320 we retrieve that from the xserver's log file.
321 -->
322 <xorg version="1.3.0">
323 <!-- driver: Data about a driver.
324 (optional)
325 attribute name: The name of the driver.
326 attribute version: The version of the driver.
327 attribute class: The module class of the driver
328 attribute device: The ID of a device driven by this driver.
329 -->
330 <driver name="fglrx" version="1.23" class="X.Org Video Driver"
331 device="12"/>
332 </xorg>
333 </software>
334
335 <!-- questions: User's answers to questions asked by the client.
336 -->
337 <questions>
338
339 <!-- question: Data of a question.
340 attribute name: The unique name of the question.
341 attribute plugin: The name of the plugin which asked
342 the question.
343 attribute version: The version of the question.
344 attribute type: Allowed values are "manual" and "automatic".
345 A "manual" question requires user input for the answer;
346 an "automatic" question gets the answer automatically.
347 -->
348 <question name="detected_network_controllers"
349 plugin="find_network_controllers">
350
351 <!-- target: Information about a device or software package the
352 question is about. The attribute "id" is the sysfs path of
353 a device as given in <udev>, or the ID of a software package
354 node.
355 This node may appear multiple times.
356 -->
357 <target id="/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/usb_endpoint/usbdev3.1_ep81">
358 <!-- driver: The driver which controls the target device. This tag
359 may appear more than once.
360
361 While we are working on a project called "Hardware Database",
362 we are not that much interested in the question, if a device
363 works "as such", but if their Linux driver(s) work.
364
365 It is not in every case possible to identify the used driver
366 from HAL data, so we need another way to add this information.
367 (example: HAL does not know, which driver is used for the
368 graphics card.)
369
370 Example for multiple drivers: Some scanners have a SCSI _and_
371 a USB interface; if such a scanner is tested, we not only want
372 to know, which Sane backend is used, but also, which interface
373 is used.
374
375 Also, it might be interesting to know for many USB 2.0 devices,
376 if a USB 1 (uhci_hcd or ohci_hcd driver) or the USB 2 driver
377 (ehci_hcd) was used. A USB 1 driver might for example explain
378 latency problems.
379 -->
380 <driver>ipw3945</driver>
381 </target>
382
383 <!-- ID of the 88E8055 PCI-E Gigabit Ethernet Controller -->
384 <target id="/devices/pci0000:00/0000:00:1f.1"/>
385
386 <!-- command: The command line of an external command required to
387 ask this question.
388 -->
389 <command/>
390
391 <!-- answer: The answer to the question. Two types of answers are
392 defined, "multiple_choice" and "measurement". (See below
393 for an example of the latter.)
394 attribute type: Must be "multiple_choice" or "measurement".
395 -->
396 <answer type="multiple_choice">pass</answer>
397
398 <!-- answer_choices: The list of possible choices.
399 The data should only be used for consistency
400 checks and to detect variants of the question.
401 -->
402 <answer_choices>
403 <value type="str">fail</value>
404 <value type="str">pass</value>
405 <value type="str">skip</value>
406 </answer_choices>
407
408 <!-- A user comment about the device or about the test.
409 -->
410 <comment>
411 The WLAN adapter drops the connection very frequently.
412 </comment>
413 </question>
414
415 <question name="internet_ping"
416 plugin="internet_ping">
417 <target id="/devices/pci0000:00/0000:00:1f.1"/>
418 <command/>
419 <answer type="multiple_choice">pass</answer>
420 <answer_choices>
421 <value type="str">fail</value>
422 <value type="str">pass</value>
423 <value type="str">skip</value>
424 </answer_choices>
425 </question>
426
427 <!-- example for a "measurement question"
428 -->
429 <question name="harddisk_speed"
430 plugin="harddisk_speed">
431 <target id="/devices/pci0000:00/0000:00:1f.1/host3/target3:0:0/3:0:0:0"/>
432 <command>hdparm -t /dev/sda</command>
433 <!-- answer: The answer to a "measurement question".
434 attribute type: See above.
435 attribute unit: The unit of the result of the measurement.
436 XXX Abel Deuring 2007-12-12 bug=175978 We should
437 enumerate a list of allowed units, in order to avoid
438 multiple units for the same dimension. e.g., B/sec,
439 MB/sec or inch, cm, foot.
440
441 For dimensionless values, the attribute unit is omitted.
442
443 "Percentage" and similar "convenience pseudo-units" like
444 ppm are _not_ allowed; instead a dimensionless
445 value must be used, where 0 is equivalent 0% and 1.0 is
446 equivalent to 100%.
447 -->
448 <answer type="measurement" unit="MB/sec">38.4</answer>
449 </question>
450 </questions>
451 <!-- miscellaneous additional text data.
452 -->
453</system>
0454
=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py'
--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_parser.py 2009-09-15 17:05:32 +0000
@@ -4,7 +4,10 @@
4"""Tests of the HWDB submissions parser."""4"""Tests of the HWDB submissions parser."""
55
6from cStringIO import StringIO6from cStringIO import StringIO
7import cElementTree as etree7try:
8 import xml.etree.cElementTree as etree
9except ImportError:
10 import cElementTree as etree
8from datetime import datetime11from datetime import datetime
9import logging12import logging
10import os13import os
@@ -507,6 +510,111 @@
507 'model': 'MD 4394'}],510 'model': 'MD 4394'}],
508 'Invalid parsing result for <aliases>')511 'Invalid parsing result for <aliases>')
509512
513 def testUdev(self):
514 """The content of the <udev> node is converted into a list of dicts.
515 """
516 parser = SubmissionParser(self.log)
517 node = etree.fromstring("""
518<udev>P: /devices/LNXSYSTM:00
519E: UDEV_LOG=3
520E: DEVPATH=/devices/LNXSYSTM:00
521E: MODALIAS=acpi:LNXSYSTM:
522
523P: /devices/pci0000:00/0000:00:1a.0
524E: UDEV_LOG=3
525E: DEVPATH=/devices/pci0000:00/0000:00:1a.0
526S: char/189:256
527</udev>
528""")
529 result = parser._parseUdev(node)
530 self.assertEqual(
531 [
532 {
533 'P': '/devices/LNXSYSTM:00',
534 'E': {
535 'UDEV_LOG': '3',
536 'DEVPATH': '/devices/LNXSYSTM:00',
537 'MODALIAS': 'acpi:LNXSYSTM:',
538 },
539 'S': [],
540 },
541 {
542 'P': '/devices/pci0000:00/0000:00:1a.0',
543 'E': {
544 'UDEV_LOG': '3',
545 'DEVPATH': '/devices/pci0000:00/0000:00:1a.0',
546 },
547 'S': ['char/189:256'],
548 },
549 ],
550 result,
551 'Invalid parsing result for <udev>')
552
553 def testUdevLineWithoutColon(self):
554 """<udev> nodes with lines not in key: value format are rejected."""
555 parser = SubmissionParser(self.log)
556 parser.submission_key = 'Detect udev lines not in key:value format'
557 node = etree.fromstring("""
558<udev>P: /devices/LNXSYSTM:00
559bad line
560</udev>
561""")
562 result = parser._parseUdev(node)
563 self.assertEqual(
564 None, result,
565 'Invalid parsing result for a <udev> node with a line not having '
566 'the key: value format.')
567 self.assertErrorMessage(
568 parser.submission_key,
569 "Line 1 in <udev>: No valid key:value data: 'bad line'")
570
571 def testUdevPropertyLineWithoutEqualSign(self):
572 """<udev> nodes with lines not in key: value format are rejected."""
573 parser = SubmissionParser(self.log)
574 parser.submission_key = (
575 'Detect udev property lines not in key=value format')
576 node = etree.fromstring("""
577<udev>P: /devices/LNXSYSTM:00
578E: bad property
579</udev>
580""")
581 result = parser._parseUdev(node)
582 self.assertEqual(
583 None, result,
584 'Invalid parsing result for a <udev> node with a property line '
585 'not having the key=value format.')
586 self.assertErrorMessage(
587 parser.submission_key,
588 "Line 1 in <udev>: Property without valid key=value data: "
589 "'E: bad property'")
590
591 def testUdevDataWithDuplicateKey(self):
592 """<udev> nodes with lines not in key: value format are rejected."""
593 parser = SubmissionParser(self.log)
594 parser.submission_key = 'Detect duplactae attributes in udev data'
595 node = etree.fromstring("""
596<udev>P: /devices/LNXSYSTM:00
597W:1
598W:2
599</udev>
600""")
601 result = parser._parseUdev(node)
602 self.assertEqual(
603 [
604 {
605 'P': '/devices/LNXSYSTM:00',
606 'E': {},
607 'S': [],
608 'W': '2',
609 },
610 ],
611 result,
612 'Invalid parsing result for a <udev> node with a duplicate '
613 'attribute.')
614 self.assertWarningMessage(
615 parser.submission_key,
616 "Line 2 in <udev>: Duplicate attribute key: 'W:2'")
617
510 def testHardware(self):618 def testHardware(self):
511 """The <hardware> tag is converted into a dictionary."""619 """The <hardware> tag is converted into a dictionary."""
512 test = self620 test = self
513621
=== modified file 'lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py'
--- lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/scripts/tests/test_hwdb_submission_validation.py 2009-09-14 17:38:57 +0000
@@ -375,8 +375,20 @@
375375
376 The only allowed tags are specified by the Relax NG schema:376 The only allowed tags are specified by the Relax NG schema:
377 live_cd, system_id, distribution, distroseries, architecture,377 live_cd, system_id, distribution, distroseries, architecture,
378 private, contactable, date_created.378 private, contactable, date_created (tested in
379 testSummaryRequiredTags()), and the optional tag <kernel-release>.
379 """380 """
381 # we can add the tag <kernel-release>
382 sample_data = self.insertSampledata(
383 data=self.sample_data,
384 insert_text='<kernel-release value="2.6.28-15-generic"/>',
385 where='</summary>')
386 result, submission_id = self.runValidator(sample_data)
387 self.assertNotEqual(
388 result, None,
389 'Valid submission containing a <kernel-release> tag rejected.')
390
391 # Adding any other tag is not possible.
380 sample_data = self.insertSampledata(392 sample_data = self.insertSampledata(
381 data=self.sample_data,393 data=self.sample_data,
382 insert_text='<nonsense/>',394 insert_text='<nonsense/>',
@@ -609,24 +621,99 @@
609 'Invalid attribute foo for element plugin',621 'Invalid attribute foo for element plugin',
610 'invalid attribute in client plugin')622 'invalid attribute in client plugin')
611623
612 def testHardwareSubTags(self):624 def testHardwareSubTagHalOrUdev(self):
625 """The <hardware> tag requires data about hardware devices.
626
627 This data is stored either in the sub-tag <hal> or in the
628 three tags <udev>, <dmi>, <sysfs-attributes>.
629 """
630 # Omitting <hal> leads to an error.
631 sample_data = self.replaceSampledata(
632 data=self.sample_data,
633 replace_text='',
634 from_text='<hal',
635 to_text='</hal>')
636 result, submission_id = self.runValidator(sample_data)
637 self.assertErrorMessage(
638 submission_id, result,
639 'Expecting an element hal, got nothing',
640 'missing tag <hal> in <hardware>')
641
642 # But we may replace <hal> by the three tags <udev>, <dmi>,
643 #<sysfs-attributes>.
644 sample_data = self.replaceSampledata(
645 data=self.sample_data,
646 replace_text="""
647 <udev>some text</udev>
648 <dmi>some text</dmi>
649 <sysfs-attributes>some text</sysfs-attributes>
650 """,
651 from_text='<hal',
652 to_text='</hal>')
653 result, submission_id = self.runValidator(sample_data)
654 self.assertNotEqual(
655 result, None,
656 'submission with valid <udev>, <dmi>, <sysfs-attributes> tags '
657 'rejected')
658
659 def testHardwareSubTagUdevIncomplete(self):
613 """The <hardware> tag has a fixed set of allowed sub-tags.660 """The <hardware> tag has a fixed set of allowed sub-tags.
614661
615 Valid sub-tags are <hal>, <processors>, <aliases>.662 Valid sub-tags are <hal>, <udev>, <dmi>, <sysfs-attributes>,
616 <aliases> is optional; <hal> and <processors> are required.663 <processors>, <aliases>. <aliases> is optional, <processors>
664 is required, and either <hal> or all three tags <udev>, <dmi>,
665 <sysfs-attributes> must be present.
617 """666 """
618 # Omitting either of the required tags leads on an error.667 # Omitting any of the three tags <udev>, <dmi>, <sysfs-attributes>
619 for tag in ('hal', 'processors'):668 # makes the data invalid.
669 all_tags = ['udev', 'dmi', 'sysfs-attributes']
670 for index, missing_tag in enumerate(all_tags):
671 test_tags = all_tags[:]
672 del test_tags[index]
673 replace_text = [
674 '<%s>text</%s>' % (tag, tag) for tag in test_tags]
675 replace_text = ''.join(replace_text)
620 sample_data = self.replaceSampledata(676 sample_data = self.replaceSampledata(
621 data=self.sample_data,677 data=self.sample_data,
622 replace_text='',678 replace_text=replace_text,
623 from_text='<%s' % tag,679 from_text='<hal',
624 to_text='</%s>' % tag)680 to_text='</hal>')
625 result, submission_id = self.runValidator(sample_data)681 result, submission_id = self.runValidator(sample_data)
626 self.assertErrorMessage(682 self.assertErrorMessage(
627 submission_id, result,683 submission_id, result,
628 'Expecting an element %s, got nothing' % tag,684 'Expecting an element %s, got nothing' % missing_tag,
629 'missing tag <%s> in <hardware>' % tag)685 'missing tag <%s> in <hardware>' % missing_tag)
686
687 def testHardwareSubTagHalMixedWithUdev(self):
688 """Mixing <hal> with <udev>, <dmi>, <sysfs-attributes> is impossible.
689 """
690 # A submission containing the tag <hal> as well as one of <udev>,
691 # <dmi>, <sysfs-attributes> is invalid.
692 for tag in ['udev', 'dmi', 'sysfs-attributes']:
693 sample_data = self.insertSampledata(
694 data=self.sample_data,
695 insert_text='<%s>some text</%s>' % (tag, tag),
696 where='<hal')
697 result, submission_id = self.runValidator(sample_data)
698 self.assertErrorMessage(
699 submission_id, result,
700 'Invalid sequence in interleave',
701 '<hal> mixed with <%s> in <hardware>' % tag)
702
703 def testHardwareOtherSubTags(self):
704 """The <hardware> tag has a fixed set of allowed sub-tags.
705 """
706 # The <processors> tag must not be omitted.
707 sample_data = self.replaceSampledata(
708 data=self.sample_data,
709 replace_text='',
710 from_text='<processors',
711 to_text='</processors>')
712 result, submission_id = self.runValidator(sample_data)
713 self.assertErrorMessage(
714 submission_id, result,
715 'Expecting an element processors, got nothing',
716 '<processor> tag omitted')
630717
631 # The <aliases> tag may be omitted.718 # The <aliases> tag may be omitted.
632 sample_data = self.replaceSampledata(719 sample_data = self.replaceSampledata(
@@ -1603,24 +1690,6 @@
1603 'Extra element aliases in interleave',1690 'Extra element aliases in interleave',
1604 'missing attribute of <alias>')1691 'missing attribute of <alias>')
16051692
1606 # target must be an integer.
1607 sample_data = self.sample_data.replace(
1608 '<alias target="65">', '<alias target="noInteger">')
1609 result, submission_id = self.runValidator(sample_data)
1610 self.assertErrorMessage(
1611 submission_id, result,
1612 'Extra element aliases in interleave',
1613 'missing attribute of <alias>')
1614
1615 # target must not be empty.
1616 sample_data = self.sample_data.replace(
1617 '<alias target="65">', '<alias target="">')
1618 result, submission_id = self.runValidator(sample_data)
1619 self.assertErrorMessage(
1620 submission_id, result,
1621 'Element hardware failed to validate content',
1622 'missing attribute of <alias>')
1623
1624 # Other attributes are not allowed. We get again the same1693 # Other attributes are not allowed. We get again the same
1625 # quite unspecific error message as above.1694 # quite unspecific error message as above.
1626 sample_data = self.sample_data.replace(1695 sample_data = self.sample_data.replace(
16271696
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2009-08-20 12:36:07 +0000
+++ lib/canonical/launchpad/security.py 2009-08-31 03:03:00 +0000
@@ -63,7 +63,7 @@
63 IMilestone, IProjectMilestone)63 IMilestone, IProjectMilestone)
64from canonical.launchpad.interfaces.oauth import (64from canonical.launchpad.interfaces.oauth import (
65 IOAuthAccessToken, IOAuthRequestToken)65 IOAuthAccessToken, IOAuthRequestToken)
66from lp.soyuz.interfaces.packageset import IPackagesetSet66from lp.soyuz.interfaces.packageset import IPackageset, IPackagesetSet
67from lp.translations.interfaces.pofile import IPOFile67from lp.translations.interfaces.pofile import IPOFile
68from lp.translations.interfaces.potemplate import (68from lp.translations.interfaces.potemplate import (
69 IPOTemplate, IPOTemplateSubset)69 IPOTemplate, IPOTemplateSubset)
@@ -195,6 +195,7 @@
195 return (user.inTeam(celebrities.registry_experts)195 return (user.inTeam(celebrities.registry_experts)
196 or user.inTeam(celebrities.admin))196 or user.inTeam(celebrities.admin))
197197
198
198class ReviewProduct(ReviewByRegistryExpertsOrAdmins):199class ReviewProduct(ReviewByRegistryExpertsOrAdmins):
199 usedfor = IProduct200 usedfor = IProduct
200201
@@ -211,8 +212,6 @@
211 usedfor = IProjectSet212 usedfor = IProjectSet
212213
213214
214
215
216class ViewPillar(AuthorizationBase):215class ViewPillar(AuthorizationBase):
217 usedfor = IPillar216 usedfor = IPillar
218 permission = 'launchpad.View'217 permission = 'launchpad.View'
@@ -385,8 +384,7 @@
385 if user.inTeam(driver):384 if user.inTeam(driver):
386 return True385 return True
387 admins = getUtility(ILaunchpadCelebrities).admin386 admins = getUtility(ILaunchpadCelebrities).admin
388 return (user.inTeam(self.obj.target.owner) or387 return (user.inTeam(targetowner) or user.inTeam(admins))
389 user.inTeam(admins))
390388
391389
392class DriverSpecification(AuthorizationBase):390class DriverSpecification(AuthorizationBase):
@@ -514,6 +512,7 @@
514 """IProjectMilestone is a fake content object."""512 """IProjectMilestone is a fake content object."""
515 return False513 return False
516514
515
517class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):516class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
518 permission = 'launchpad.Edit'517 permission = 'launchpad.Edit'
519 usedfor = IMilestone518 usedfor = IMilestone
@@ -2268,6 +2267,18 @@
2268 or user.inTeam(celebrities.admin))2267 or user.inTeam(celebrities.admin))
22692268
22702269
2270class EditPackageset(AuthorizationBase):
2271 permission = 'launchpad.Edit'
2272 usedfor = IPackageset
2273
2274 def checkAuthenticated(self, user):
2275 """The owner of a package set can edit the object."""
2276 celebrities = getUtility(ILaunchpadCelebrities)
2277 return (
2278 user.inTeam(self.obj.owner)
2279 or user.inTeam(celebrities.admin))
2280
2281
2271class EditPackagesetSet(AuthorizationBase):2282class EditPackagesetSet(AuthorizationBase):
2272 permission = 'launchpad.Edit'2283 permission = 'launchpad.Edit'
2273 usedfor = IPackagesetSet2284 usedfor = IPackagesetSet
22742285
=== removed file 'lib/canonical/launchpad/templates/bugbranch-delete.pt'
--- lib/canonical/launchpad/templates/bugbranch-delete.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/bugbranch-delete.pt 1970-01-01 00:00:00 +0000
@@ -1,28 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xml:lang="en"
4 lang="en"
5 metal:use-macro="context/@@main_template/master"
6 i18n:domain="launchpad">
7
8<body>
9
10<metal:leftportlets fill-slot="portlets">
11 <div tal:replace="structure context/branch/@@+portlet-details" />
12</metal:leftportlets>
13
14<div metal:fill-slot="main">
15
16 <h1>Delete bug branch link</h1>
17
18 <div class="documentDescription">
19 Are you sure you want to remove the link between...
20 </div>
21
22 <div metal:use-macro="context/@@launchpad_form/form">
23 </div>
24
25</div>
26
27</body>
28</html>
290
=== modified file 'lib/canonical/launchpad/templates/launchpad-form.pt'
--- lib/canonical/launchpad/templates/launchpad-form.pt 2009-08-01 02:04:55 +0000
+++ lib/canonical/launchpad/templates/launchpad-form.pt 2009-09-03 15:39:22 +0000
@@ -14,11 +14,6 @@
14 enctype="multipart/form-data"14 enctype="multipart/form-data"
15 accept-charset="UTF-8">15 accept-charset="UTF-8">
1616
17 <h1 tal:condition="view/label"
18 tal:content="view/label"
19 metal:define-slot="heading"
20 >Add Something</h1>
21
22 <div metal:define-macro="formbody">17 <div metal:define-macro="formbody">
2318
24 <p metal:define-slot="extra_info" tal:replace="nothing">19 <p metal:define-slot="extra_info" tal:replace="nothing">
@@ -38,8 +33,8 @@
38 Schema validation errors.33 Schema validation errors.
39 </p>34 </p>
4035
41 <div class="row" 36 <div class="row"
42 metal:define-slot="extra_top" 37 metal:define-slot="extra_top"
43 tal:replace="nothing">38 tal:replace="nothing">
44 <div>Extra top</div>39 <div>Extra top</div>
45 <div><input type="text"/></div>40 <div><input type="text"/></div>
@@ -55,7 +50,7 @@
55 tal:content="structure script" />50 tal:content="structure script" />
5651
57 <div class="row"52 <div class="row"
58 metal:define-slot="extra_bottom" 53 metal:define-slot="extra_bottom"
59 tal:replace="nothing">54 tal:replace="nothing">
60 <div>Extra bottom</div>55 <div>Extra bottom</div>
61 <div class="field"><input type="text" /></div>56 <div class="field"><input type="text" /></div>
@@ -73,8 +68,8 @@
73 </tal:has-cancel-link>68 </tal:has-cancel-link>
74 </div>69 </div>
7570
76 <div class="row" 71 <div class="row"
77 metal:define-slot="extra_buttons" 72 metal:define-slot="extra_buttons"
78 tal:replace="nothing">73 tal:replace="nothing">
79 </div>74 </div>
8075
8176
=== modified file 'lib/canonical/launchpad/templates/logintoken-claimprofile.pt'
--- lib/canonical/launchpad/templates/logintoken-claimprofile.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-claimprofile.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12<body>9<body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-claimteam.pt'
--- lib/canonical/launchpad/templates/logintoken-claimteam.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-claimteam.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12<body>9<body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-index.pt'
--- lib/canonical/launchpad/templates/logintoken-index.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-index.pt 2009-09-17 18:46:13 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
129
@@ -14,11 +11,9 @@
1411
15<div metal:fill-slot="main">12<div metal:fill-slot="main">
1613
17 <h1>Confirmation already concluded</h1>
18
19 <p>14 <p>
20 You reached this page probably because you followed a link received by15 You reached this page probably because you followed a link received by
21 email. That link was sent to confirm you have access to the email 16 email. That link was sent to confirm you have access to the email
22 address it was sent to, but this confirmation was already concluded, so17 address it was sent to, but this confirmation was already concluded, so
23 you don't need to do anything else.18 you don't need to do anything else.
24 </p>19 </p>
2520
=== modified file 'lib/canonical/launchpad/templates/logintoken-newaccount.pt'
--- lib/canonical/launchpad/templates/logintoken-newaccount.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-newaccount.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-resetpassword.pt'
--- lib/canonical/launchpad/templates/logintoken-resetpassword.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-resetpassword.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-validateemail.pt'
--- lib/canonical/launchpad/templates/logintoken-validateemail.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-validateemail.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-validategpg.pt'
--- lib/canonical/launchpad/templates/logintoken-validategpg.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-validategpg.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
1310
=== modified file 'lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt'
--- lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-validatesignonlygpg.pt 2009-09-17 18:46:13 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
@@ -15,10 +12,6 @@
1512
16 <div metal:use-macro="context/@@launchpad_form/form">13 <div metal:use-macro="context/@@launchpad_form/form">
1714
18 <metal:heading fill-slot="heading">
19 <h1>Confirm your OpenPGP key</h1>
20 </metal:heading>
21
22 <p metal:fill-slot="extra_info">15 <p metal:fill-slot="extra_info">
23 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.16 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.
24 </p>17 </p>
2518
=== modified file 'lib/canonical/launchpad/templates/logintoken-validateteamemail.pt'
--- lib/canonical/launchpad/templates/logintoken-validateteamemail.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/logintoken-validateteamemail.pt 2009-09-16 20:51:57 +0000
@@ -3,10 +3,7 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/locationless"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12 <body>9 <body>
1310
=== modified file 'lib/canonical/launchpad/templates/main-template.pt'
--- lib/canonical/launchpad/templates/main-template.pt 2009-08-30 13:31:02 +0000
+++ lib/canonical/launchpad/templates/main-template.pt 2009-09-15 17:30:59 +0000
@@ -98,12 +98,6 @@
98 <input type="search" id="search-text" name="field.text" />98 <input type="search" id="search-text" name="field.text" />
99 </form>99 </form>
100 <tal:hierarchy replace="structure context/@@+hierarchy" />100 <tal:hierarchy replace="structure context/@@+hierarchy" />
101 <div id="globalheader" xml:lang="en" lang="en" dir="ltr"
102 tal:condition="site_message">
103 <div class="sitemessage" tal:content="structure site_message">
104 This site is running pre-release code.
105 </div>
106 </div>
107 <div101 <div
108 tal:condition="view/macro:pagehas/applicationtabs"102 tal:condition="view/macro:pagehas/applicationtabs"
109 tal:define="facetmenu view/menu:facet"103 tal:define="facetmenu view/menu:facet"
@@ -239,6 +233,10 @@
239 <a tal:condition="request/lp:person" href="/feedback"233 <a tal:condition="request/lp:person" href="/feedback"
240 >Contact us</a> | <a href="https://help.launchpad.net/">Get help with Launchpad</a>234 >Contact us</a> | <a href="https://help.launchpad.net/">Get help with Launchpad</a>
241 </div>235 </div>
236
237 <metal:site-message
238 use-macro="context/@@+base-layout-macros/site-message"/>
239
242 <div id="lp-arcana">240 <div id="lp-arcana">
243 &copy;&nbsp;2004-2009&nbsp;<a241 &copy;&nbsp;2004-2009&nbsp;<a
244 href="http://canonical.com/">Canonical&nbsp;Ltd.</a>242 href="http://canonical.com/">Canonical&nbsp;Ltd.</a>
245243
=== modified file 'lib/canonical/launchpad/templates/structural-subscriptions-manage.pt'
--- lib/canonical/launchpad/templates/structural-subscriptions-manage.pt 2009-07-17 17:59:07 +0000
+++ lib/canonical/launchpad/templates/structural-subscriptions-manage.pt 2009-09-17 17:12:58 +0000
@@ -3,41 +3,28 @@
3 xmlns:tal="http://xml.zope.org/namespaces/tal"3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"6 metal:use-macro="view/macro:page/main_side"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="context/@@main_template/master"
10 i18n:domain="launchpad"7 i18n:domain="launchpad"
11>8>
12<body>9<body>
1310 <div metal:fill-slot="main">
14<metal:leftportlets fill-slot="portlets_one">11 <p>
15 <div tal:replace="structure context/@@+portlet-malone-bugmail-filtering-faq"/>12 You can choose to receive an e-mail every time someone reports or
16</metal:leftportlets>13 changes a public bug associated with
1714 <span tal:replace="context/title">this item</span>.
18<metal:rightportlets fill-slot="portlets_two">15 </p>
19 <div tal:condition="view/show_details_portlet"16 <p>
20 tal:replace="structure context/@@+portlet-details" />17 <strong>Important:</strong> subscribing here may mean you receive a
21 <div tal:replace="structure context/@@+portlet-structural-subscribers" />18 great deal of e-mail. You can return here to unsubscribe at any
22</metal:rightportlets>19 time.
2320 </p>
24<metal:heading fill-slot="pageheading">21 <div metal:use-macro="context/@@launchpad_form/form" />
25 <h1>Change bug subscriptions</h1>22 </div>
26</metal:heading>23 <div metal:fill-slot="side">
2724 <div tal:replace="structure context/@@+portlet-malone-bugmail-filtering-faq"/>
28<div metal:fill-slot="main">25 <div tal:condition="view/show_details_portlet"
29 26 tal:replace="structure context/@@+portlet-details" />
30 <p>27 <div tal:replace="structure context/@@+portlet-structural-subscribers" />
31 You can choose to receive an e-mail every time someone reports or28 </div>
32 changes a public bug associated with
33 <span tal:replace="context/title">this item</span>.
34 </p>
35 <p>
36 <strong>Important:</strong> subscribing here may mean you receive a
37 great deal of e-mail. You can return here to unsubscribe at any
38 time.
39 </p>
40 <div metal:use-macro="context/@@launchpad_form/form" />
41</div>
42</body>29</body>
43</html>30</html>
4431
=== modified file 'lib/canonical/launchpad/testing/fakepackager.py'
--- lib/canonical/launchpad/testing/fakepackager.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/testing/fakepackager.py 2009-09-14 04:07:18 +0000
@@ -354,7 +354,7 @@
354 'Selected upstream directory does not exist: %s' % (354 'Selected upstream directory does not exist: %s' % (
355 os.path.basename(self.upstream_directory)))355 os.path.basename(self.upstream_directory)))
356356
357 debuild_options = ['-S']357 debuild_options = ['--no-conf', '-S']
358358
359 if not signed:359 if not signed:
360 debuild_options.extend(['-uc', '-us'])360 debuild_options.extend(['-uc', '-us'])
361361
=== modified file 'lib/canonical/launchpad/testing/pages.py'
--- lib/canonical/launchpad/testing/pages.py 2009-08-27 19:55:58 +0000
+++ lib/canonical/launchpad/testing/pages.py 2009-09-01 09:54:54 +0000
@@ -258,7 +258,8 @@
258 for col_num, item in enumerate(row.findAll('td')):258 for col_num, item in enumerate(row.findAll('td')):
259 if columns is None or col_num in columns:259 if columns is None or col_num in columns:
260 row_content.append(extract_text(item))260 row_content.append(extract_text(item))
261 print sep.join(row_content)261 if len(row_content) > 0:
262 print sep.join(row_content)
262263
263def print_radio_button_field(content, name):264def print_radio_button_field(content, name):
264 """Find the input called field.name, and print a friendly representation.265 """Find the input called field.name, and print a friendly representation.
265266
=== modified file 'lib/canonical/launchpad/utilities/searchservice.py'
--- lib/canonical/launchpad/utilities/searchservice.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/utilities/searchservice.py 2009-09-04 10:43:39 +0000
@@ -13,7 +13,10 @@
13 'PageMatches',13 'PageMatches',
14 ]14 ]
1515
16import cElementTree as ET16try:
17 import xml.etree.cElementTree as ET
18except ImportError:
19 import cElementTree as ET
17import urllib20import urllib
18from urlparse import urlunparse21from urlparse import urlunparse
1922
2023
=== modified file 'lib/canonical/launchpad/utilities/unicode_csv.py'
--- lib/canonical/launchpad/utilities/unicode_csv.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/utilities/unicode_csv.py 2009-09-04 11:42:13 +0000
@@ -41,6 +41,17 @@
41class UnicodeCSVReader:41class UnicodeCSVReader:
42 """A CSV reader that reads encoded files and yields unicode."""42 """A CSV reader that reads encoded files and yields unicode."""
4343
44 class DelegateLineNumAccessDescriptor:
45 """The Python 2.5 DictReader expects its reader to support access to a
46 line_num attribute, therefore to keep UnicodeCSVReader capable of being
47 used within a DictReader we provide a line_num attribute which
48 delegates to the real reader."""
49
50 def __get__(self, obj, type):
51 return obj.reader.line_num
52
53 line_num = DelegateLineNumAccessDescriptor()
54
44 def __init__(self, file_, dialect=csv.excel, encoding="utf-8", **kwds):55 def __init__(self, file_, dialect=csv.excel, encoding="utf-8", **kwds):
45 file_ = UTF8Recoder(file_, encoding)56 file_ = UTF8Recoder(file_, encoding)
46 self.reader = csv.reader(file_, dialect=dialect, **kwds)57 self.reader = csv.reader(file_, dialect=dialect, **kwds)
4758
=== modified file 'lib/canonical/launchpad/versioninfo.py'
--- lib/canonical/launchpad/versioninfo.py 2009-06-25 05:30:52 +0000
+++ lib/canonical/launchpad/versioninfo.py 2009-09-02 19:11:01 +0000
@@ -1,7 +1,7 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Give access to bzr version info, if available.4"""Give access to bzr and other version info, if available.
55
6The bzr version info file is expected to be in the Launchpad root in the6The bzr version info file is expected to be in the Launchpad root in the
7file bzr-version-info.py.7file bzr-version-info.py.
@@ -16,19 +16,28 @@
16If the bzr-version-info.py file does not exist, then revno, date and16If the bzr-version-info.py file does not exist, then revno, date and
17branch_nick will all be None.17branch_nick will all be None.
1818
19If that file exists, and contains valid python, revno, date and branch_nick19If that file exists, and contains valid Python, revno, date and branch_nick
20will have appropriate values from version_info.20will have appropriate values from version_info.
2121
22If that file exists, and contains invalid python, there will be an error when22If that file exists, and contains invalid Python, there will be an error when
23this module is loaded. This module is imported into23this module is loaded. This module is imported into
24canonical/launchpad/__init__.py so that such errors are caught at start-up.24canonical/launchpad/__init__.py so that such errors are caught at start-up.
2525
26This module also reads version.txt at the top of the tree (i.e. a sibling of
27bzr-version-info.py), which contains the Launchpad release number. If that
28file does not exist, we make something up.
26"""29"""
2730
31__all__ = [
32 'branch_nick',
33 'date',
34 'revno',
35 'versioninfo',
36 ]
37
38
28import imp39import imp
2940
30__all__ = ['versioninfo', 'revno', 'date', 'branch_nick']
31
3241
33def read_version_info():42def read_version_info():
34 try:43 try:
@@ -52,3 +61,14 @@
52 date = versioninfo.get('date')61 date = versioninfo.get('date')
53 branch_nick = versioninfo.get('branch_nick')62 branch_nick = versioninfo.get('branch_nick')
5463
64
65try:
66 version_file = open('version.txt')
67except IOError:
68 release = 'x.y.z'
69else:
70 try:
71 version_data = version_file.read()
72 release = version_data.strip()
73 finally:
74 version_file.close()
5575
=== modified file 'lib/canonical/launchpad/webapp/breadcrumb.py'
--- lib/canonical/launchpad/webapp/breadcrumb.py 2009-08-25 12:35:23 +0000
+++ lib/canonical/launchpad/webapp/breadcrumb.py 2009-09-18 04:06:03 +0000
@@ -7,6 +7,9 @@
77
8__all__ = [8__all__ = [
9 'Breadcrumb',9 'Breadcrumb',
10 'DisplaynameBreadcrumb',
11 'NameBreadcrumb',
12 'TitleBreadcrumb',
10 ]13 ]
1114
1215
@@ -27,6 +30,7 @@
27 implements(IBreadcrumb)30 implements(IBreadcrumb)
2831
29 text = None32 text = None
33 _url = None
3034
31 def __init__(self, context):35 def __init__(self, context):
32 self.context = context36 self.context = context
@@ -46,20 +50,35 @@
4650
47 @property51 @property
48 def url(self):52 def url(self):
49 return canonical_url(self.context, rootsite=self.rootsite)53 if self._url is None:
5054 return canonical_url(self.context, rootsite=self.rootsite)
51 @property55 else:
52 def icon(self):56 return self._url
53 """See `IBreadcrumb`."""
54 # Get the <img> tag from the path adapter.
55 return queryAdapter(
56 self.context, IPathAdapter, name='image').icon()
5757
58 def __repr__(self):58 def __repr__(self):
59 if self.icon is not None:59 return "<%s url='%s' text='%s'>" % (
60 icon_repr = " icon='%s'" % self.icon60 self.__class__.__name__, self.url, self.text)
61 else:61
62 icon_repr = ""62
6363class NameBreadcrumb(Breadcrumb):
64 return "<%s url='%s' text='%s'%s>" % (64 """An `IBreadcrumb` that uses the context's name as its text."""
65 self.__class__.__name__, self.url, self.text, icon_repr)65
66 @property
67 def text(self):
68 return self.context.name
69
70
71class DisplaynameBreadcrumb(Breadcrumb):
72 """An `IBreadcrumb` that uses the context's displayname as its text."""
73
74 @property
75 def text(self):
76 return self.context.displayname
77
78
79class TitleBreadcrumb(Breadcrumb):
80 """An `IBreadcrumb` that uses the context's title as its text."""
81
82 @property
83 def text(self):
84 return self.context.title
6685
=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml 2009-08-27 09:00:17 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml 2009-09-11 15:26:11 +0000
@@ -365,6 +365,13 @@
365 />365 />
366366
367 <adapter367 <adapter
368 for="lp.registry.interfaces.distroseries.IDistroSeries"
369 provides="zope.traversing.interfaces.IPathAdapter"
370 factory="canonical.launchpad.webapp.tales.DistroSeriesFormatterAPI"
371 name="fmt"
372 />
373
374 <adapter
368 for="canonical.launchpad.interfaces.IBug"375 for="canonical.launchpad.interfaces.IBug"
369 provides="zope.traversing.interfaces.IPathAdapter"376 provides="zope.traversing.interfaces.IPathAdapter"
370 factory="canonical.launchpad.webapp.tales.BugFormatterAPI"377 factory="canonical.launchpad.webapp.tales.BugFormatterAPI"
@@ -507,6 +514,24 @@
507 name="fmt"514 name="fmt"
508 />515 />
509 <adapter516 <adapter
517 for="lp.translations.interfaces.translationgroup.ITranslationGroup"
518 provides="zope.traversing.interfaces.IPathAdapter"
519 factory="canonical.launchpad.webapp.tales.TranslationGroupFormatterAPI"
520 name="fmt"
521 />
522 <adapter
523 for="lp.services.worlddata.interfaces.language.ILanguage"
524 provides="zope.traversing.interfaces.IPathAdapter"
525 factory="canonical.launchpad.webapp.tales.LanguageFormatterAPI"
526 name="fmt"
527 />
528 <adapter
529 for="lp.translations.interfaces.pofile.IPOFile"
530 provides="zope.traversing.interfaces.IPathAdapter"
531 factory="canonical.launchpad.webapp.tales.POFileFormatterAPI"
532 name="fmt"
533 />
534 <adapter
510 for="*"535 for="*"
511 provides="zope.traversing.interfaces.IPathAdapter"536 provides="zope.traversing.interfaces.IPathAdapter"
512 factory="canonical.launchpad.webapp.tales.PermissionRequiredQuery"537 factory="canonical.launchpad.webapp.tales.PermissionRequiredQuery"
513538
=== modified file 'lib/canonical/launchpad/webapp/error.py'
--- lib/canonical/launchpad/webapp/error.py 2009-07-17 18:46:25 +0000
+++ lib/canonical/launchpad/webapp/error.py 2009-09-08 22:42:42 +0000
@@ -2,6 +2,16 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
5__all__ = [
6 'InvalidBatchSizeView',
7 'NotFoundView',
8 'ProtocolErrorView',
9 'ReadOnlyErrorView',
10 'RequestExpiredView',
11 'SystemErrorView',
12 'TranslationUnavailableView',
13 ]
14
515
6import sys16import sys
The diff has been truncated for viewing.