Merge lp:~leonardr/launchpad/grant-permissions-oauth into lp:launchpad/db-devel

Proposed by Leonard Richardson
Status: Merged
Approved by: Edwin Grubbs
Approved revision: 11095
Merge reported by: Leonard Richardson
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpad/grant-permissions-oauth
Merge into: lp:launchpad/db-devel
Diff against target: 1583 lines (+503/-318)
25 files modified
bootstrap.py (+76/-24)
lib/canonical/launchpad/browser/oauth.py (+41/-14)
lib/canonical/launchpad/components/decoratedresultset.py (+0/-4)
lib/canonical/launchpad/doc/webapp-authorization.txt (+21/-2)
lib/canonical/launchpad/pagetests/oauth/authorize-token.txt (+28/-3)
lib/canonical/launchpad/webapp/interfaces.py (+9/-0)
lib/lp/archivepublisher/utils.py (+2/-1)
lib/lp/archiveuploader/dscfile.py (+131/-106)
lib/lp/archiveuploader/nascentupload.py (+0/-7)
lib/lp/archiveuploader/tests/test_dscfile.py (+58/-41)
lib/lp/archiveuploader/tests/test_nascentuploadfile.py (+1/-2)
lib/lp/hardwaredb/model/hwdb.py (+1/-10)
lib/lp/registry/browser/distribution.py (+1/-12)
lib/lp/registry/browser/sourcepackage.py (+1/-1)
lib/lp/registry/browser/team.py (+2/-2)
lib/lp/registry/model/distroseries.py (+3/-7)
lib/lp/registry/model/mailinglist.py (+15/-13)
lib/lp/registry/tests/test_mailinglist.py (+29/-29)
lib/lp/registry/vocabularies.py (+2/-11)
lib/lp/soyuz/doc/package-diff.txt (+1/-6)
lib/lp/soyuz/scripts/initialise_distroseries.py (+27/-7)
lib/lp/soyuz/scripts/tests/test_initialise_distroseries.py (+28/-10)
lib/lp/testing/fakelibrarian.py (+14/-3)
lib/lp/testing/tests/test_fakelibrarian.py (+9/-0)
versions.cfg (+3/-3)
To merge this branch: bzr merge lp:~leonardr/launchpad/grant-permissions-oauth
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) Approve
Review via email: mp+29425@code.launchpad.net

Description of the change

This branch introduces a brand new access level for OAuth tokens: GRANT_PERMISSIONS. GRANT_PERMISSIONS is different from other access levels (eg. READ_PUBLIC) in that it's designed to be used by one specific application: the forthcoming desktop credential manager for the Launchpad web service.

Currently GRANT_PERMISSIONS acts exactly like READ_PUBLIC, with the following exceptions:

1. GRANT_PERMISSIONS is not published in the list of access levels -- the client must know its name ahead of time.

2. GRANT_PERMISSIONS does not show up on the list of access levels in +authorize-token unless it is specifically requested and the _only_ access level the client requests. You can't let the end-user choose between WRITE_PRIVATE and GRANT_PERMISSIONS -- either your program needs GRANT_PERMISSIONS or it doesn't.

Eventually there will be a third exception:

3. GRANT_PERMISSIONS will be the only access level that can access the current user's list of OAuth access tokens, or invoke the named operation to create a new OAuth access token.

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Leonard,

This looks good. I just have a couple of wording suggestions below.

-Edwin

>=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
>--- lib/canonical/launchpad/webapp/interfaces.py 2010-05-02 23:43:35 + 0000
>+++ lib/canonical/launchpad/webapp/interfaces.py 2010-07-07 20:53:55 + 0000
>@@ -547,6 +547,16 @@
> for reading and changing anything, including private data.
> """)
>
>+ GRANT_PERMISSIONS = DBItem(60, """
>+ Grant Permissions
>+
>+ Not only will the application will be able to access Launchpad

s/will the application will be/will the application be/

>+ on your behalf, it will be able to grant access to your
>+ Launchpad account to any other application. This is a very
>+ powerful level of access. You should not grant this level of
>+ access to any application except the official desktop
>+ Launchpad credential manager.

"official Launchpad desktop..." makes more sense than "official desktop
Launchpad ...".

>+ """)
>
> class AccessLevel(DBEnumeratedType):
> """The level of access any given principal has."""
>

review: Approve
11096. By Leonard Richardson

Rewording in response to feedback.

11097. By Leonard Richardson

Merge from trunk.

11098. By Leonard Richardson

Merge from trunk.

Revision history for this message
Brad Crittenden (bac) wrote :

Leonard can this branch be landed now? Is there anything blocking you?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bootstrap.py'
--- bootstrap.py 2010-03-20 01:13:25 +0000
+++ bootstrap.py 2010-08-24 16:45:57 +0000
@@ -1,6 +1,6 @@
1##############################################################################1##############################################################################
2#2#
3# Copyright (c) 2006 Zope Corporation and Contributors.3# Copyright (c) 2006 Zope Foundation and Contributors.
4# All Rights Reserved.4# All Rights Reserved.
5#5#
6# This software is subject to the provisions of the Zope Public License,6# This software is subject to the provisions of the Zope Public License,
@@ -16,11 +16,9 @@
16Simply run this script in a directory containing a buildout.cfg.16Simply run this script in a directory containing a buildout.cfg.
17The script accepts buildout command-line options, so you can17The script accepts buildout command-line options, so you can
18use the -c option to specify an alternate configuration file.18use the -c option to specify an alternate configuration file.
19
20$Id$
21"""19"""
2220
23import os, shutil, sys, tempfile, textwrap, urllib, urllib221import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
24from optparse import OptionParser22from optparse import OptionParser
2523
26if sys.platform == 'win32':24if sys.platform == 'win32':
@@ -32,11 +30,23 @@
32else:30else:
33 quote = str31 quote = str
3432
33# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
34stdout, stderr = subprocess.Popen(
35 [sys.executable, '-Sc',
36 'try:\n'
37 ' import ConfigParser\n'
38 'except ImportError:\n'
39 ' print 1\n'
40 'else:\n'
41 ' print 0\n'],
42 stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
43has_broken_dash_S = bool(int(stdout.strip()))
44
35# In order to be more robust in the face of system Pythons, we want to45# In order to be more robust in the face of system Pythons, we want to
36# run without site-packages loaded. This is somewhat tricky, in46# run without site-packages loaded. This is somewhat tricky, in
37# particular because Python 2.6's distutils imports site, so starting47# particular because Python 2.6's distutils imports site, so starting
38# with the -S flag is not sufficient. However, we'll start with that:48# with the -S flag is not sufficient. However, we'll start with that:
39if 'site' in sys.modules:49if not has_broken_dash_S and 'site' in sys.modules:
40 # We will restart with python -S.50 # We will restart with python -S.
41 args = sys.argv[:]51 args = sys.argv[:]
42 args[0:0] = [sys.executable, '-S']52 args[0:0] = [sys.executable, '-S']
@@ -109,13 +119,22 @@
109 help=("Specify a directory for storing eggs. Defaults to "119 help=("Specify a directory for storing eggs. Defaults to "
110 "a temporary directory that is deleted when the "120 "a temporary directory that is deleted when the "
111 "bootstrap script completes."))121 "bootstrap script completes."))
122parser.add_option("-t", "--accept-buildout-test-releases",
123 dest='accept_buildout_test_releases',
124 action="store_true", default=False,
125 help=("Normally, if you do not specify a --version, the "
126 "bootstrap script and buildout gets the newest "
127 "*final* versions of zc.buildout and its recipes and "
128 "extensions for you. If you use this flag, "
129 "bootstrap and buildout will get the newest releases "
130 "even if they are alphas or betas."))
112parser.add_option("-c", None, action="store", dest="config_file",131parser.add_option("-c", None, action="store", dest="config_file",
113 help=("Specify the path to the buildout configuration "132 help=("Specify the path to the buildout configuration "
114 "file to be used."))133 "file to be used."))
115134
116options, args = parser.parse_args()135options, args = parser.parse_args()
117136
118# if -c was provided, we push it back into args for buildout' main function137# if -c was provided, we push it back into args for buildout's main function
119if options.config_file is not None:138if options.config_file is not None:
120 args += ['-c', options.config_file]139 args += ['-c', options.config_file]
121140
@@ -130,16 +149,15 @@
130 else:149 else:
131 options.setup_source = setuptools_source150 options.setup_source = setuptools_source
132151
133args = args + ['bootstrap']152if options.accept_buildout_test_releases:
134153 args.append('buildout:accept-buildout-test-releases=true')
154args.append('bootstrap')
135155
136try:156try:
137 to_reload = False
138 import pkg_resources157 import pkg_resources
139 to_reload = True158 import setuptools # A flag. Sometimes pkg_resources is installed alone.
140 if not hasattr(pkg_resources, '_distribute'):159 if not hasattr(pkg_resources, '_distribute'):
141 raise ImportError160 raise ImportError
142 import setuptools # A flag. Sometimes pkg_resources is installed alone.
143except ImportError:161except ImportError:
144 ez_code = urllib2.urlopen(162 ez_code = urllib2.urlopen(
145 options.setup_source).read().replace('\r\n', '\n')163 options.setup_source).read().replace('\r\n', '\n')
@@ -151,10 +169,8 @@
151 if options.use_distribute:169 if options.use_distribute:
152 setup_args['no_fake'] = True170 setup_args['no_fake'] = True
153 ez['use_setuptools'](**setup_args)171 ez['use_setuptools'](**setup_args)
154 if to_reload:172 reload(sys.modules['pkg_resources'])
155 reload(pkg_resources)173 import pkg_resources
156 else:
157 import pkg_resources
158 # This does not (always?) update the default working set. We will174 # This does not (always?) update the default working set. We will
159 # do it.175 # do it.
160 for path in sys.path:176 for path in sys.path:
@@ -167,23 +183,59 @@
167 '-mqNxd',183 '-mqNxd',
168 quote(eggs_dir)]184 quote(eggs_dir)]
169185
170if options.download_base:186if not has_broken_dash_S:
171 cmd.extend(['-f', quote(options.download_base)])187 cmd.insert(1, '-S')
172188
173requirement = 'zc.buildout'189find_links = options.download_base
174if options.version:190if not find_links:
175 requirement = '=='.join((requirement, options.version))191 find_links = os.environ.get('bootstrap-testing-find-links')
176cmd.append(requirement)192if find_links:
193 cmd.extend(['-f', quote(find_links)])
177194
178if options.use_distribute:195if options.use_distribute:
179 setup_requirement = 'distribute'196 setup_requirement = 'distribute'
180else:197else:
181 setup_requirement = 'setuptools'198 setup_requirement = 'setuptools'
182ws = pkg_resources.working_set199ws = pkg_resources.working_set
200setup_requirement_path = ws.find(
201 pkg_resources.Requirement.parse(setup_requirement)).location
183env = dict(202env = dict(
184 os.environ,203 os.environ,
185 PYTHONPATH=ws.find(204 PYTHONPATH=setup_requirement_path)
186 pkg_resources.Requirement.parse(setup_requirement)).location)205
206requirement = 'zc.buildout'
207version = options.version
208if version is None and not options.accept_buildout_test_releases:
209 # Figure out the most recent final version of zc.buildout.
210 import setuptools.package_index
211 _final_parts = '*final-', '*final'
212 def _final_version(parsed_version):
213 for part in parsed_version:
214 if (part[:1] == '*') and (part not in _final_parts):
215 return False
216 return True
217 index = setuptools.package_index.PackageIndex(
218 search_path=[setup_requirement_path])
219 if find_links:
220 index.add_find_links((find_links,))
221 req = pkg_resources.Requirement.parse(requirement)
222 if index.obtain(req) is not None:
223 best = []
224 bestv = None
225 for dist in index[req.project_name]:
226 distv = dist.parsed_version
227 if _final_version(distv):
228 if bestv is None or distv > bestv:
229 best = [dist]
230 bestv = distv
231 elif distv == bestv:
232 best.append(dist)
233 if best:
234 best.sort()
235 version = best[-1].version
236if version:
237 requirement = '=='.join((requirement, version))
238cmd.append(requirement)
187239
188if is_jython:240if is_jython:
189 import subprocess241 import subprocess
@@ -193,7 +245,7 @@
193if exitcode != 0:245if exitcode != 0:
194 sys.stdout.flush()246 sys.stdout.flush()
195 sys.stderr.flush()247 sys.stderr.flush()
196 print ("An error occured when trying to install zc.buildout. "248 print ("An error occurred when trying to install zc.buildout. "
197 "Look above this message for any errors that "249 "Look above this message for any errors that "
198 "were output by easy_install.")250 "were output by easy_install.")
199 sys.exit(exitcode)251 sys.exit(exitcode)
200252
=== modified file 'lib/canonical/launchpad/browser/oauth.py'
--- lib/canonical/launchpad/browser/oauth.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/browser/oauth.py 2010-08-24 16:45:57 +0000
@@ -88,8 +88,13 @@
8888
89 token = consumer.newRequestToken()89 token = consumer.newRequestToken()
90 if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:90 if self.request.headers.get('Accept') == HTTPResource.JSON_TYPE:
91 # Don't show the client the GRANT_PERMISSIONS access
92 # level. If they have a legitimate need to use it, they'll
93 # already know about it.
94 permissions = [permission for permission in OAuthPermission.items
95 if permission != OAuthPermission.GRANT_PERMISSIONS]
91 return self.getJSONRepresentation(96 return self.getJSONRepresentation(
92 OAuthPermission.items, token, include_secret=True)97 permissions, token, include_secret=True)
93 return u'oauth_token=%s&oauth_token_secret=%s' % (98 return u'oauth_token=%s&oauth_token_secret=%s' % (
94 token.key, token.secret)99 token.key, token.secret)
95100
@@ -100,6 +105,7 @@
100def create_oauth_permission_actions():105def create_oauth_permission_actions():
101 """Return a list of `Action`s for each possible `OAuthPermission`."""106 """Return a list of `Action`s for each possible `OAuthPermission`."""
102 actions = Actions()107 actions = Actions()
108 actions_excluding_grant_permissions = Actions()
103 def success(form, action, data):109 def success(form, action, data):
104 form.reviewToken(action.permission)110 form.reviewToken(action.permission)
105 for permission in OAuthPermission.items:111 for permission in OAuthPermission.items:
@@ -108,13 +114,15 @@
108 condition=token_exists_and_is_not_reviewed)114 condition=token_exists_and_is_not_reviewed)
109 action.permission = permission115 action.permission = permission
110 actions.append(action)116 actions.append(action)
111 return actions117 if permission != OAuthPermission.GRANT_PERMISSIONS:
112118 actions_excluding_grant_permissions.append(action)
119 return actions, actions_excluding_grant_permissions
113120
114class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):121class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
115 """Where users authorize consumers to access Launchpad on their behalf."""122 """Where users authorize consumers to access Launchpad on their behalf."""
116123
117 actions = create_oauth_permission_actions()124 actions, actions_excluding_grant_permissions = (
125 create_oauth_permission_actions())
118 label = "Authorize application to access Launchpad on your behalf"126 label = "Authorize application to access Launchpad on your behalf"
119 schema = IOAuthRequestToken127 schema = IOAuthRequestToken
120 field_names = []128 field_names = []
@@ -132,28 +140,47 @@
132 acceptable subset of OAuthPermission.140 acceptable subset of OAuthPermission.
133141
134 The user always has the option to deny the client access142 The user always has the option to deny the client access
135 altogether, so it makes sense for the client to specify the143 altogether, so it makes sense for the client to ask for the
136 least restrictions possible.144 least access possible.
137145
138 If the client sends nonsensical values for allow_permissions,146 If the client sends nonsensical values for allow_permissions,
139 the end-user will be given an unrestricted choice.147 the end-user will be given a choice among all the permissions
148 used by normal applications.
140 """149 """
150
141 allowed_permissions = self.request.form_ng.getAll('allow_permission')151 allowed_permissions = self.request.form_ng.getAll('allow_permission')
142 if len(allowed_permissions) == 0:152 if len(allowed_permissions) == 0:
143 return self.actions153 return self.actions_excluding_grant_permissions
144 actions = Actions()154 actions = Actions()
155
156 # UNAUTHORIZED is always one of the options. If the client
157 # explicitly requested UNAUTHORIZED, remove it from the list
158 # to simplify the algorithm: we'll add it back later.
159 if OAuthPermission.UNAUTHORIZED.name in allowed_permissions:
160 allowed_permissions.remove(OAuthPermission.UNAUTHORIZED.name)
161
162 # GRANT_PERMISSIONS cannot be requested as one of several
163 # options--it must be the only option (other than
164 # UNAUTHORIZED). If GRANT_PERMISSIONS is one of several
165 # options, remove it from the list.
166 if (OAuthPermission.GRANT_PERMISSIONS.name in allowed_permissions
167 and len(allowed_permissions) > 1):
168 allowed_permissions.remove(OAuthPermission.GRANT_PERMISSIONS.name)
169
145 for action in self.actions:170 for action in self.actions:
146 if (action.permission.name in allowed_permissions171 if (action.permission.name in allowed_permissions
147 or action.permission is OAuthPermission.UNAUTHORIZED):172 or action.permission is OAuthPermission.UNAUTHORIZED):
148 actions.append(action)173 actions.append(action)
174
149 if len(list(actions)) == 1:175 if len(list(actions)) == 1:
150 # The only visible action is UNAUTHORIZED. That means the176 # The only visible action is UNAUTHORIZED. That means the
151 # client tried to restrict the actions but didn't name any177 # client tried to restrict the permissions but didn't name
152 # actual actions (except possibly UNAUTHORIZED). Rather178 # any actual permissions (except possibly
153 # than present the end-user with an impossible situation179 # UNAUTHORIZED). Rather than present the end-user with an
154 # where their only option is to deny access, we'll present180 # impossible situation where their only option is to deny
155 # the full range of actions.181 # access, we'll present the full range of actions (except
156 return self.actions182 # for GRANT_PERMISSIONS).
183 return self.actions_excluding_grant_permissions
157 return actions184 return actions
158185
159 def initialize(self):186 def initialize(self):
160187
=== modified file 'lib/canonical/launchpad/components/decoratedresultset.py'
--- lib/canonical/launchpad/components/decoratedresultset.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/components/decoratedresultset.py 2010-08-24 16:45:57 +0000
@@ -9,7 +9,6 @@
9 ]9 ]
1010
11from lazr.delegates import delegates11from lazr.delegates import delegates
12from storm.expr import Column
13from storm.zope.interfaces import IResultSet12from storm.zope.interfaces import IResultSet
14from zope.security.proxy import removeSecurityProxy13from zope.security.proxy import removeSecurityProxy
1514
@@ -31,9 +30,6 @@
3130
32 This behaviour is required for other classes as well (Distribution,31 This behaviour is required for other classes as well (Distribution,
33 DistroArchSeries), hence a generalised solution.32 DistroArchSeries), hence a generalised solution.
34
35 This class also fixes a bug currently in Storm's ResultSet.count
36 method (see below)
37 """33 """
38 delegates(IResultSet, context='result_set')34 delegates(IResultSet, context='result_set')
3935
4036
=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
--- lib/canonical/launchpad/doc/webapp-authorization.txt 2010-04-16 15:06:55 +0000
+++ lib/canonical/launchpad/doc/webapp-authorization.txt 2010-08-24 16:45:57 +0000
@@ -79,8 +79,27 @@
79 >>> check_permission('launchpad.View', bug_1)79 >>> check_permission('launchpad.View', bug_1)
80 False80 False
8181
82Users logged in through the web application, though, have full access,82Now consider a principal authorized to create OAuth tokens. Whenever
83which means they can read/change any object they have access to.83it's not creating OAuth tokens, it has a level of permission
84equivalent to READ_PUBLIC.
85
86 >>> principal.access_level = AccessLevel.GRANT_PERMISSIONS
87 >>> setupInteraction(principal)
88 >>> check_permission('launchpad.View', bug_1)
89 False
90
91 >>> check_permission('launchpad.Edit', sample_person)
92 False
93
94This may seem useless from a security standpoint, since once a
95malicious client is authorized to create OAuth tokens, it can escalate
96its privileges at any time by creating a new token for itself. The
97security benefit is more subtle: by discouraging feature creep in
98clients that have this super-access level, we reduce the risk that a
99bug in a _trusted_ client will enable privilege escalation attacks.
100
101Users logged in through the web application have full access, which
102means they can read/change any object they have access to.
84103
85 >>> mock_participation = Participation()104 >>> mock_participation = Participation()
86 >>> login('test@canonical.com', mock_participation)105 >>> login('test@canonical.com', mock_participation)
87106
=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-02-05 13:25:46 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-08-24 16:45:57 +0000
@@ -44,7 +44,8 @@
44 ...44 ...
45 See all applications authorized to access Launchpad on your behalf.45 See all applications authorized to access Launchpad on your behalf.
4646
47This page contains one submit button for each item of OAuthPermission.47This page contains one submit button for each item of OAuthPermission,
48except for 'Grant Permissions', which must be specifically requested.
4849
49 >>> browser.getControl('No Access')50 >>> browser.getControl('No Access')
50 <SubmitControl...51 <SubmitControl...
@@ -57,9 +58,14 @@
57 >>> browser.getControl('Change Anything')58 >>> browser.getControl('Change Anything')
58 <SubmitControl...59 <SubmitControl...
5960
61 >>> browser.getControl('Grant Permissions')
62 Traceback (most recent call last):
63 ...
64 LookupError: label 'Grant Permissions'
65
60 >>> actions = main_content.findAll('input', attrs={'type': 'submit'})66 >>> actions = main_content.findAll('input', attrs={'type': 'submit'})
61 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission67 >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
62 >>> len(actions) == len(OAuthPermission.items)68 >>> len(actions) == len(OAuthPermission.items) - 1
63 True69 True
6470
65An application, when asking to access Launchpad on a user's behalf,71An application, when asking to access Launchpad on a user's behalf,
@@ -83,9 +89,28 @@
83 Change Non-Private Data89 Change Non-Private Data
84 Change Anything90 Change Anything
8591
92The only time the 'Grant Permissions' permission shows up in this list
93is if the client specifically requests it, and no other
94permission. (Also requesting UNAUTHORIZED is okay--it will show up
95anyway.)
96
97 >>> print_access_levels('allow_permission=GRANT_PERMISSIONS')
98 No Access
99 Grant Permissions
100
101 >>> print_access_levels(
102 ... 'allow_permission=GRANT_PERMISSIONS&allow_permission=UNAUTHORIZED')
103 No Access
104 Grant Permissions
105
106 >>> print_access_levels(
107 ... 'allow_permission=WRITE_PUBLIC&allow_permission=GRANT_PERMISSIONS')
108 No Access
109 Change Non-Private Data
110
86If an application doesn't specify any valid access levels, or only111If an application doesn't specify any valid access levels, or only
87specifies the UNAUTHORIZED access level, Launchpad will show all the112specifies the UNAUTHORIZED access level, Launchpad will show all the
88access levels.113access levels, except for GRANT_PERMISSIONS.
89114
90 >>> print_access_levels('')115 >>> print_access_levels('')
91 No Access116 No Access
92117
=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
--- lib/canonical/launchpad/webapp/interfaces.py 2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/interfaces.py 2010-08-24 16:45:57 +0000
@@ -527,6 +527,15 @@
527 for reading and changing anything, including private data.527 for reading and changing anything, including private data.
528 """)528 """)
529529
530 GRANT_PERMISSIONS = DBItem(60, """
531 Grant Permissions
532
533 The application will be able to grant access to your Launchpad
534 account to any other application. This is a very powerful
535 level of access. You should not grant this level of access to
536 any application except the official Launchpad credential
537 manager.
538 """)
530539
531class AccessLevel(DBEnumeratedType):540class AccessLevel(DBEnumeratedType):
532 """The level of access any given principal has."""541 """The level of access any given principal has."""
533542
=== modified file 'lib/lp/archivepublisher/utils.py'
--- lib/lp/archivepublisher/utils.py 2010-08-20 20:31:18 +0000
+++ lib/lp/archivepublisher/utils.py 2010-08-24 16:45:57 +0000
@@ -109,7 +109,8 @@
109 end = start + chunk_size109 end = start + chunk_size
110110
111 # The reason why we listify the sliced ResultSet is because we111 # The reason why we listify the sliced ResultSet is because we
112 # cannot very it's size using 'count' (see bug #217644). However,112 # cannot very it's size using 'count' (see bug #217644 and note
113 # that it was fixed in storm but not SQLObjectResultSet). However,
113 # It's not exactly a problem considering non-empty set will be114 # It's not exactly a problem considering non-empty set will be
114 # iterated anyway.115 # iterated anyway.
115 batch = list(self.input[start:end])116 batch = list(self.input[start:end])
116117
=== modified file 'lib/lp/archiveuploader/dscfile.py'
--- lib/lp/archiveuploader/dscfile.py 2010-08-21 13:54:20 +0000
+++ lib/lp/archiveuploader/dscfile.py 2010-08-24 16:45:57 +0000
@@ -13,10 +13,11 @@
13 'SignableTagFile',13 'SignableTagFile',
14 'DSCFile',14 'DSCFile',
15 'DSCUploadedFile',15 'DSCUploadedFile',
16 'findChangelog',16 'find_changelog',
17 'findCopyright',17 'find_copyright',
18 ]18 ]
1919
20from cStringIO import StringIO
20import errno21import errno
21import glob22import glob
22import os23import os
@@ -73,6 +74,70 @@
73from lp.soyuz.interfaces.sourcepackageformat import SourcePackageFormat74from lp.soyuz.interfaces.sourcepackageformat import SourcePackageFormat
7475
7576
77class DpkgSourceError(Exception):
78
79 _fmt = "Unable to unpack source package (%(result)s): %(output)s"
80
81 def __init__(self, output, result):
82 Exception.__init__(
83 self, self._fmt % {"output": output, "result": result})
84 self.output = output
85 self.result = result
86
87
88def unpack_source(dsc_filepath):
89 """Unpack a source package into a temporary directory
90
91 :param dsc_filepath: Path to the dsc file
92 :return: Path to the temporary directory with the unpacked sources
93 """
94 # Get a temporary dir together.
95 unpacked_dir = tempfile.mkdtemp()
96 try:
97 # chdir into it
98 cwd = os.getcwd()
99 os.chdir(unpacked_dir)
100 try:
101 args = ["dpkg-source", "-sn", "-x", dsc_filepath]
102 dpkg_source = subprocess.Popen(args, stdout=subprocess.PIPE,
103 stderr=subprocess.PIPE)
104 output, unused = dpkg_source.communicate()
105 result = dpkg_source.wait()
106 finally:
107 # When all is said and done, chdir out again so that we can
108 # clean up the tree with shutil.rmtree without leaving the
109 # process in a directory we're trying to remove.
110 os.chdir(cwd)
111
112 if result != 0:
113 dpkg_output = prefix_multi_line_string(output, " ")
114 raise DpkgSourceError(result=result, output=dpkg_output)
115 except:
116 shutil.rmtree(unpacked_dir)
117 raise
118
119 return unpacked_dir
120
121
122def cleanup_unpacked_dir(unpacked_dir):
123 """Remove the directory with an unpacked source package.
124
125 :param unpacked_dir: Path to the directory.
126 """
127 try:
128 shutil.rmtree(unpacked_dir)
129 except OSError, error:
130 if errno.errorcode[error.errno] != 'EACCES':
131 raise UploadError(
132 "couldn't remove tmp dir %s: code %s" % (
133 unpacked_dir, error.errno))
134 else:
135 result = os.system("chmod -R u+rwx " + unpacked_dir)
136 if result != 0:
137 raise UploadError("chmod failed with %s" % result)
138 shutil.rmtree(unpacked_dir)
139
140
76class SignableTagFile:141class SignableTagFile:
77 """Base class for signed file verification."""142 """Base class for signed file verification."""
78143
@@ -160,7 +225,7 @@
160 "rfc2047": rfc2047,225 "rfc2047": rfc2047,
161 "name": name,226 "name": name,
162 "email": email,227 "email": email,
163 "person": person228 "person": person,
164 }229 }
165230
166231
@@ -187,9 +252,9 @@
187252
188 # Note that files is actually only set inside verify().253 # Note that files is actually only set inside verify().
189 files = None254 files = None
190 # Copyright and changelog_path are only set inside unpackAndCheckSource().255 # Copyright and changelog are only set inside unpackAndCheckSource().
191 copyright = None256 copyright = None
192 changelog_path = None257 changelog = None
193258
194 def __init__(self, filepath, digest, size, component_and_section,259 def __init__(self, filepath, digest, size, component_and_section,
195 priority, package, version, changes, policy, logger):260 priority, package, version, changes, policy, logger):
@@ -238,12 +303,9 @@
238 else:303 else:
239 self.processSignature()304 self.processSignature()
240305
241 self.unpacked_dir = None
242
243 #306 #
244 # Useful properties.307 # Useful properties.
245 #308 #
246
247 @property309 @property
248 def source(self):310 def source(self):
249 """Return the DSC source name."""311 """Return the DSC source name."""
@@ -277,12 +339,11 @@
277 #339 #
278 # DSC file checks.340 # DSC file checks.
279 #341 #
280
281 def verify(self):342 def verify(self):
282 """Verify the uploaded .dsc file.343 """Verify the uploaded .dsc file.
283344
284 This method is an error generator, i.e, it returns an iterator over all345 This method is an error generator, i.e, it returns an iterator over
285 exceptions that are generated while processing DSC file checks.346 all exceptions that are generated while processing DSC file checks.
286 """347 """
287348
288 for error in SourceUploadFile.verify(self):349 for error in SourceUploadFile.verify(self):
@@ -518,82 +579,53 @@
518 self.logger.debug(579 self.logger.debug(
519 "Verifying uploaded source package by unpacking it.")580 "Verifying uploaded source package by unpacking it.")
520581
521 # Get a temporary dir together.
522 self.unpacked_dir = tempfile.mkdtemp()
523
524 # chdir into it
525 cwd = os.getcwd()
526 os.chdir(self.unpacked_dir)
527 dsc_in_tmpdir = os.path.join(self.unpacked_dir, self.filename)
528
529 package_files = self.files + [self]
530 try:582 try:
531 for source_file in package_files:583 unpacked_dir = unpack_source(self.filepath)
532 os.symlink(584 except DpkgSourceError, e:
533 source_file.filepath,
534 os.path.join(self.unpacked_dir, source_file.filename))
535 args = ["dpkg-source", "-sn", "-x", dsc_in_tmpdir]
536 dpkg_source = subprocess.Popen(args, stdout=subprocess.PIPE,
537 stderr=subprocess.PIPE)
538 output, unused = dpkg_source.communicate()
539 result = dpkg_source.wait()
540 finally:
541 # When all is said and done, chdir out again so that we can
542 # clean up the tree with shutil.rmtree without leaving the
543 # process in a directory we're trying to remove.
544 os.chdir(cwd)
545
546 if result != 0:
547 dpkg_output = prefix_multi_line_string(output, " ")
548 yield UploadError(585 yield UploadError(
549 "dpkg-source failed for %s [return: %s]\n"586 "dpkg-source failed for %s [return: %s]\n"
550 "[dpkg-source output: %s]"587 "[dpkg-source output: %s]"
551 % (self.filename, result, dpkg_output))588 % (self.filename, e.result, e.output))
552589 return
553 # Copy debian/copyright file content. It will be stored in the590
554 # SourcePackageRelease records.591 try:
555592 # Copy debian/copyright file content. It will be stored in the
556 # Check if 'dpkg-source' created only one directory.593 # SourcePackageRelease records.
557 temp_directories = [594
558 dirname for dirname in os.listdir(self.unpacked_dir)595 # Check if 'dpkg-source' created only one directory.
559 if os.path.isdir(dirname)]596 temp_directories = [
560 if len(temp_directories) > 1:597 dirname for dirname in os.listdir(unpacked_dir)
561 yield UploadError(598 if os.path.isdir(dirname)]
562 'Unpacked source contains more than one directory: %r'599 if len(temp_directories) > 1:
563 % temp_directories)600 yield UploadError(
564601 'Unpacked source contains more than one directory: %r'
565 # XXX cprov 20070713: We should access only the expected directory602 % temp_directories)
566 # name (<sourcename>-<no_epoch(no_revision(version))>).603
567604 # XXX cprov 20070713: We should access only the expected directory
568 # Locate both the copyright and changelog files for later processing.605 # name (<sourcename>-<no_epoch(no_revision(version))>).
569 for error in findCopyright(self, self.unpacked_dir, self.logger):606
570 yield error607 # Locate both the copyright and changelog files for later
571608 # processing.
572 for error in findChangelog(self, self.unpacked_dir, self.logger):609 try:
573 yield error610 self.copyright = find_copyright(unpacked_dir, self.logger)
574611 except UploadError, error:
575 self.logger.debug("Cleaning up source tree.")612 yield error
613 return
614 except UploadWarning, warning:
615 yield warning
616
617 try:
618 self.changelog = find_changelog(unpacked_dir, self.logger)
619 except UploadError, error:
620 yield error
621 return
622 except UploadWarning, warning:
623 yield warning
624 finally:
625 self.logger.debug("Cleaning up source tree.")
626 cleanup_unpacked_dir(unpacked_dir)
576 self.logger.debug("Done")627 self.logger.debug("Done")
577628
578 def cleanUp(self):
579 if self.unpacked_dir is None:
580 return
581 try:
582 shutil.rmtree(self.unpacked_dir)
583 except OSError, error:
584 # XXX: dsilvers 2006-03-15: We currently lack a test for this.
585 if errno.errorcode[error.errno] != 'EACCES':
586 raise UploadError(
587 "%s: couldn't remove tmp dir %s: code %s" % (
588 self.filename, self.unpacked_dir, error.errno))
589 else:
590 result = os.system("chmod -R u+rwx " + self.unpacked_dir)
591 if result != 0:
592 raise UploadError("chmod failed with %s" % result)
593 shutil.rmtree(self.unpacked_dir)
594 self.unpacked_dir = None
595
596
597 def findBuild(self):629 def findBuild(self):
598 """Find and return the SourcePackageRecipeBuild, if one is specified.630 """Find and return the SourcePackageRecipeBuild, if one is specified.
599631
@@ -651,8 +683,8 @@
651683
652 changelog_lfa = self.librarian.create(684 changelog_lfa = self.librarian.create(
653 "changelog",685 "changelog",
654 os.stat(self.changelog_path).st_size,686 len(self.changelog),
655 open(self.changelog_path, "r"),687 StringIO(self.changelog),
656 "text/x-debian-source-changelog",688 "text/x-debian-source-changelog",
657 restricted=self.policy.archive.private)689 restricted=self.policy.archive.private)
658690
@@ -716,6 +748,7 @@
716 validation inside DSCFile.verify(); there is no748 validation inside DSCFile.verify(); there is no
717 store_in_database() method.749 store_in_database() method.
718 """750 """
751
719 def __init__(self, filepath, digest, size, policy, logger):752 def __init__(self, filepath, digest, size, policy, logger):
720 component_and_section = priority = "--no-value--"753 component_and_section = priority = "--no-value--"
721 NascentUploadFile.__init__(754 NascentUploadFile.__init__(
@@ -735,7 +768,7 @@
735768
736 :param source_file: The directory where the source was extracted769 :param source_file: The directory where the source was extracted
737 :param source_dir: The directory where the source was extracted.770 :param source_dir: The directory where the source was extracted.
738 :return fullpath: The full path of the file, else return None if the 771 :return fullpath: The full path of the file, else return None if the
739 file is not found.772 file is not found.
740 """773 """
741 # Instead of trying to predict the unpacked source directory name,774 # Instead of trying to predict the unpacked source directory name,
@@ -758,50 +791,42 @@
758 return fullpath791 return fullpath
759 return None792 return None
760793
761def findCopyright(dsc_file, source_dir, logger):794
795def find_copyright(source_dir, logger):
762 """Find and store any debian/copyright.796 """Find and store any debian/copyright.
763797
764 :param dsc_file: A DSCFile object where the copyright will be stored.
765 :param source_dir: The directory where the source was extracted.798 :param source_dir: The directory where the source was extracted.
766 :param logger: A logger object for debug output.799 :param logger: A logger object for debug output.
800 :return: Contents of copyright file
767 """801 """
768 try:802 copyright_file = findFile(source_dir, 'debian/copyright')
769 copyright_file = findFile(source_dir, 'debian/copyright')
770 except UploadError, error:
771 yield error
772 return
773 if copyright_file is None:803 if copyright_file is None:
774 yield UploadWarning("No copyright file found.")804 raise UploadWarning("No copyright file found.")
775 return
776805
777 logger.debug("Copying copyright contents.")806 logger.debug("Copying copyright contents.")
778 dsc_file.copyright = open(copyright_file).read().strip()807 return open(copyright_file).read().strip()
779808
780809
781def findChangelog(dsc_file, source_dir, logger):810def find_changelog(source_dir, logger):
782 """Find and move any debian/changelog.811 """Find and move any debian/changelog.
783812
784 This function finds the changelog file within the source package. The813 This function finds the changelog file within the source package. The
785 changelog file is later uploaded to the librarian by814 changelog file is later uploaded to the librarian by
786 DSCFile.storeInDatabase().815 DSCFile.storeInDatabase().
787816
788 :param dsc_file: A DSCFile object where the copyright will be stored.
789 :param source_dir: The directory where the source was extracted.817 :param source_dir: The directory where the source was extracted.
790 :param logger: A logger object for debug output.818 :param logger: A logger object for debug output.
819 :return: Changelog contents
791 """820 """
792 try:821 changelog_file = findFile(source_dir, 'debian/changelog')
793 changelog_file = findFile(source_dir, 'debian/changelog')
794 except UploadError, error:
795 yield error
796 return
797 if changelog_file is None:822 if changelog_file is None:
798 # Policy requires debian/changelog to always exist.823 # Policy requires debian/changelog to always exist.
799 yield UploadError("No changelog file found.")824 raise UploadError("No changelog file found.")
800 return
801825
802 # Move the changelog file out of the package direcotry826 # Move the changelog file out of the package direcotry
803 logger.debug("Found changelog")827 logger.debug("Found changelog")
804 dsc_file.changelog_path = changelog_file828 return open(changelog_file, 'r').read()
829
805830
806831
807def check_format_1_0_files(filename, file_type_counts, component_counts,832def check_format_1_0_files(filename, file_type_counts, component_counts,
808833
=== modified file 'lib/lp/archiveuploader/nascentupload.py'
--- lib/lp/archiveuploader/nascentupload.py 2010-08-20 20:31:18 +0000
+++ lib/lp/archiveuploader/nascentupload.py 2010-08-24 16:45:57 +0000
@@ -893,12 +893,6 @@
893 'Exception while accepting:\n %s' % e, exc_info=True)893 'Exception while accepting:\n %s' % e, exc_info=True)
894 self.do_reject(notify)894 self.do_reject(notify)
895 return False895 return False
896 else:
897 self.cleanUp()
898
899 def cleanUp(self):
900 if self.changes.dsc is not None:
901 self.changes.dsc.cleanUp()
902896
903 def do_reject(self, notify=True):897 def do_reject(self, notify=True):
904 """Reject the current upload given the reason provided."""898 """Reject the current upload given the reason provided."""
@@ -929,7 +923,6 @@
929 self.queue_root.notify(summary_text=self.rejection_message,923 self.queue_root.notify(summary_text=self.rejection_message,
930 changes_file_object=changes_file_object, logger=self.logger)924 changes_file_object=changes_file_object, logger=self.logger)
931 changes_file_object.close()925 changes_file_object.close()
932 self.cleanUp()
933926
934 def _createQueueEntry(self):927 def _createQueueEntry(self):
935 """Return a PackageUpload object."""928 """Return a PackageUpload object."""
936929
=== modified file 'lib/lp/archiveuploader/tests/test_dscfile.py'
--- lib/lp/archiveuploader/tests/test_dscfile.py 2010-08-20 20:31:18 +0000
+++ lib/lp/archiveuploader/tests/test_dscfile.py 2010-08-24 16:45:57 +0000
@@ -10,10 +10,12 @@
10from canonical.launchpad.scripts.logger import QuietFakeLogger10from canonical.launchpad.scripts.logger import QuietFakeLogger
11from canonical.testing.layers import LaunchpadZopelessLayer11from canonical.testing.layers import LaunchpadZopelessLayer
12from lp.archiveuploader.dscfile import (12from lp.archiveuploader.dscfile import (
13 cleanup_unpacked_dir,
13 DSCFile,14 DSCFile,
14 findChangelog,15 find_changelog,
15 findCopyright,16 find_copyright,
16 format_to_file_checker_map,17 format_to_file_checker_map,
18 unpack_source,
17 )19 )
18from lp.archiveuploader.nascentuploadfile import UploadError20from lp.archiveuploader.nascentuploadfile import UploadError
19from lp.archiveuploader.tests import (21from lp.archiveuploader.tests import (
@@ -37,9 +39,6 @@
3739
38class TestDscFile(TestCase):40class TestDscFile(TestCase):
3941
40 class MockDSCFile:
41 copyright = None
42
43 def setUp(self):42 def setUp(self):
44 super(TestDscFile, self).setUp()43 super(TestDscFile, self).setUp()
45 self.tmpdir = self.makeTemporaryDirectory()44 self.tmpdir = self.makeTemporaryDirectory()
@@ -47,7 +46,6 @@
47 os.makedirs(self.dir_path)46 os.makedirs(self.dir_path)
48 self.copyright_path = os.path.join(self.dir_path, "copyright")47 self.copyright_path = os.path.join(self.dir_path, "copyright")
49 self.changelog_path = os.path.join(self.dir_path, "changelog")48 self.changelog_path = os.path.join(self.dir_path, "changelog")
50 self.dsc_file = self.MockDSCFile()
5149
52 def testBadDebianCopyright(self):50 def testBadDebianCopyright(self):
53 """Test that a symlink as debian/copyright will fail.51 """Test that a symlink as debian/copyright will fail.
@@ -56,14 +54,10 @@
56 dangling symlink in an attempt to try and access files on the system54 dangling symlink in an attempt to try and access files on the system
57 processing the source packages."""55 processing the source packages."""
58 os.symlink("/etc/passwd", self.copyright_path)56 os.symlink("/etc/passwd", self.copyright_path)
59 errors = list(findCopyright(57 error = self.assertRaises(
60 self.dsc_file, self.tmpdir, mock_logger_quiet))58 UploadError, find_copyright, self.tmpdir, mock_logger_quiet)
61
62 self.assertEqual(len(errors), 1)
63 self.assertIsInstance(errors[0], UploadError)
64 self.assertEqual(59 self.assertEqual(
65 errors[0].args[0],60 error.args[0], "Symbolic link for debian/copyright not allowed")
66 "Symbolic link for debian/copyright not allowed")
6761
68 def testGoodDebianCopyright(self):62 def testGoodDebianCopyright(self):
69 """Test that a proper copyright file will be accepted"""63 """Test that a proper copyright file will be accepted"""
@@ -72,11 +66,8 @@
72 file.write(copyright)66 file.write(copyright)
73 file.close()67 file.close()
7468
75 errors = list(findCopyright(69 self.assertEquals(
76 self.dsc_file, self.tmpdir, mock_logger_quiet))70 copyright, find_copyright(self.tmpdir, mock_logger_quiet))
77
78 self.assertEqual(len(errors), 0)
79 self.assertEqual(self.dsc_file.copyright, copyright)
8071
81 def testBadDebianChangelog(self):72 def testBadDebianChangelog(self):
82 """Test that a symlink as debian/changelog will fail.73 """Test that a symlink as debian/changelog will fail.
@@ -85,14 +76,10 @@
85 dangling symlink in an attempt to try and access files on the system76 dangling symlink in an attempt to try and access files on the system
86 processing the source packages."""77 processing the source packages."""
87 os.symlink("/etc/passwd", self.changelog_path)78 os.symlink("/etc/passwd", self.changelog_path)
88 errors = list(findChangelog(79 error = self.assertRaises(
89 self.dsc_file, self.tmpdir, mock_logger_quiet))80 UploadError, find_changelog, self.tmpdir, mock_logger_quiet)
90
91 self.assertEqual(len(errors), 1)
92 self.assertIsInstance(errors[0], UploadError)
93 self.assertEqual(81 self.assertEqual(
94 errors[0].args[0],82 error.args[0], "Symbolic link for debian/changelog not allowed")
95 "Symbolic link for debian/changelog not allowed")
9683
97 def testGoodDebianChangelog(self):84 def testGoodDebianChangelog(self):
98 """Test that a proper changelog file will be accepted"""85 """Test that a proper changelog file will be accepted"""
@@ -101,12 +88,8 @@
101 file.write(changelog)88 file.write(changelog)
102 file.close()89 file.close()
10390
104 errors = list(findChangelog(91 self.assertEquals(
105 self.dsc_file, self.tmpdir, mock_logger_quiet))92 changelog, find_changelog(self.tmpdir, mock_logger_quiet))
106
107 self.assertEqual(len(errors), 0)
108 self.assertEqual(self.dsc_file.changelog_path,
109 self.changelog_path)
11093
111 def testOversizedFile(self):94 def testOversizedFile(self):
112 """Test that a file larger than 10MiB will fail.95 """Test that a file larger than 10MiB will fail.
@@ -125,13 +108,10 @@
125 file.write(empty_file)108 file.write(empty_file)
126 file.close()109 file.close()
127110
128 errors = list(findChangelog(111 error = self.assertRaises(
129 self.dsc_file, self.tmpdir, mock_logger_quiet))112 UploadError, find_changelog, self.tmpdir, mock_logger_quiet)
130
131 self.assertIsInstance(errors[0], UploadError)
132 self.assertEqual(113 self.assertEqual(
133 errors[0].args[0],114 error.args[0], "debian/changelog file too large, 10MiB max")
134 "debian/changelog file too large, 10MiB max")
135115
136116
137class TestDscFileLibrarian(TestCaseWithFactory):117class TestDscFileLibrarian(TestCaseWithFactory):
@@ -141,6 +121,7 @@
141121
142 def getDscFile(self, name):122 def getDscFile(self, name):
143 dsc_path = datadir(os.path.join('suite', name, name + '.dsc'))123 dsc_path = datadir(os.path.join('suite', name, name + '.dsc'))
124
144 class Changes:125 class Changes:
145 architectures = ['source']126 architectures = ['source']
146 logger = QuietFakeLogger()127 logger = QuietFakeLogger()
@@ -157,10 +138,7 @@
157 os.chmod(tempdir, 0555)138 os.chmod(tempdir, 0555)
158 try:139 try:
159 dsc_file = self.getDscFile('bar_1.0-1')140 dsc_file = self.getDscFile('bar_1.0-1')
160 try:141 list(dsc_file.verify())
161 list(dsc_file.verify())
162 finally:
163 dsc_file.cleanUp()
164 finally:142 finally:
165 os.chmod(tempdir, 0755)143 os.chmod(tempdir, 0755)
166144
@@ -292,3 +270,42 @@
292 # A 3.0 (native) source with component tarballs is invalid.270 # A 3.0 (native) source with component tarballs is invalid.
293 self.assertErrorsForFiles(271 self.assertErrorsForFiles(
294 [self.wrong_files_error], {NATIVE_TARBALL: 1}, {'foo': 1})272 [self.wrong_files_error], {NATIVE_TARBALL: 1}, {'foo': 1})
273
274
275class UnpackedDirTests(TestCase):
276 """Tests for unpack_source and cleanup_unpacked_dir."""
277
278 def test_unpack_source(self):
279 # unpack_source unpacks in a temporary directory and returns the
280 # path.
281 unpacked_dir = unpack_source(
282 datadir(os.path.join('suite', 'bar_1.0-1', 'bar_1.0-1.dsc')))
283 try:
284 self.assertEquals(["bar-1.0"], os.listdir(unpacked_dir))
285 self.assertContentEqual(
286 ["THIS_IS_BAR", "debian"],
287 os.listdir(os.path.join(unpacked_dir, "bar-1.0")))
288 finally:
289 cleanup_unpacked_dir(unpacked_dir)
290
291 def test_cleanup(self):
292 # cleanup_dir removes the temporary directory and all files under it.
293 temp_dir = self.makeTemporaryDirectory()
294 unpacked_dir = os.path.join(temp_dir, "unpacked")
295 os.mkdir(unpacked_dir)
296 os.mkdir(os.path.join(unpacked_dir, "bar_1.0"))
297 cleanup_unpacked_dir(unpacked_dir)
298 self.assertFalse(os.path.exists(unpacked_dir))
299
300 def test_cleanup_invalid_mode(self):
301 # cleanup_dir can remove a directory even if the mode does
302 # not allow it.
303 temp_dir = self.makeTemporaryDirectory()
304 unpacked_dir = os.path.join(temp_dir, "unpacked")
305 os.mkdir(unpacked_dir)
306 bar_path = os.path.join(unpacked_dir, "bar_1.0")
307 os.mkdir(bar_path)
308 os.chmod(bar_path, 0600)
309 os.chmod(unpacked_dir, 0600)
310 cleanup_unpacked_dir(unpacked_dir)
311 self.assertFalse(os.path.exists(unpacked_dir))
295312
=== modified file 'lib/lp/archiveuploader/tests/test_nascentuploadfile.py'
--- lib/lp/archiveuploader/tests/test_nascentuploadfile.py 2010-08-21 13:54:20 +0000
+++ lib/lp/archiveuploader/tests/test_nascentuploadfile.py 2010-08-24 16:45:57 +0000
@@ -169,8 +169,7 @@
169 uploadfile = self.createDSCFile(169 uploadfile = self.createDSCFile(
170 "foo.dsc", dsc, "main/net", "extra", "dulwich", "0.42",170 "foo.dsc", dsc, "main/net", "extra", "dulwich", "0.42",
171 self.createChangesFile("foo.changes", changes))171 self.createChangesFile("foo.changes", changes))
172 (uploadfile.changelog_path, changelog_digest, changelog_size) = (172 uploadfile.changelog = "DUMMY"
173 self.writeUploadFile("changelog", "DUMMY"))
174 uploadfile.files = []173 uploadfile.files = []
175 release = uploadfile.storeInDatabase(None)174 release = uploadfile.storeInDatabase(None)
176 self.assertEquals("0.42", release.version)175 self.assertEquals("0.42", release.version)
177176
=== modified file 'lib/lp/hardwaredb/model/hwdb.py'
--- lib/lp/hardwaredb/model/hwdb.py 2010-08-20 20:31:18 +0000
+++ lib/lp/hardwaredb/model/hwdb.py 2010-08-24 16:45:57 +0000
@@ -64,9 +64,6 @@
64 SQLBase,64 SQLBase,
65 sqlvalues,65 sqlvalues,
66 )66 )
67from canonical.launchpad.components.decoratedresultset import (
68 DecoratedResultSet,
69 )
70from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities67from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
71from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet68from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
72from canonical.launchpad.validators.name import valid_name69from canonical.launchpad.validators.name import valid_name
@@ -347,13 +344,7 @@
347 # DISTINCT clause.344 # DISTINCT clause.
348 result_set.config(distinct=True)345 result_set.config(distinct=True)
349 result_set.order_by(HWSubmission.id)346 result_set.order_by(HWSubmission.id)
350 # The Storm implementation of ResultSet.count() is incorrect if347 return result_set
351 # the select query uses the distinct directive (see bug #217644).
352 # DecoratedResultSet solves this problem by modifying the query
353 # to count only the records appearing in a subquery.
354 # We don't actually need to transform the results, which is why
355 # the second argument is a no-op.
356 return DecoratedResultSet(result_set, lambda result: result)
357348
358 def _submissionsSubmitterSelects(349 def _submissionsSubmitterSelects(
359 self, target_column, bus, vendor_id, product_id, driver_name,350 self, target_column, bus, vendor_id, product_id, driver_name,
360351
=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/distribution.py 2010-08-24 16:45:57 +0000
@@ -474,18 +474,7 @@
474 """See `AbstractPackageSearchView`."""474 """See `AbstractPackageSearchView`."""
475475
476 if self.search_by_binary_name:476 if self.search_by_binary_name:
477 non_exact_matches = self.context.searchBinaryPackages(self.text)477 return self.context.searchBinaryPackages(self.text)
478
479 # XXX Michael Nelson 20090605 bug=217644
480 # We are only using a decorated resultset here to conveniently
481 # get around the storm bug whereby count returns the count
482 # of non-distinct results, even though this result set
483 # is configured for distinct results.
484 def dummy_func(result):
485 return result
486 non_exact_matches = DecoratedResultSet(
487 non_exact_matches, dummy_func)
488
489 else:478 else:
490 non_exact_matches = self.context.searchSourcePackageCaches(479 non_exact_matches = self.context.searchSourcePackageCaches(
491 self.text)480 self.text)
492481
=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/sourcepackage.py 2010-08-24 16:45:57 +0000
@@ -542,7 +542,7 @@
542 self.form_fields = Fields(542 self.form_fields = Fields(
543 Choice(__name__='upstream',543 Choice(__name__='upstream',
544 title=_('Registered upstream project'),544 title=_('Registered upstream project'),
545 default=None,545 default=self.other_upstream,
546 vocabulary=upstream_vocabulary,546 vocabulary=upstream_vocabulary,
547 required=True))547 required=True))
548548
549549
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/team.py 2010-08-24 16:45:57 +0000
@@ -150,7 +150,7 @@
150 "name", "visibility", "displayname", "contactemail",150 "name", "visibility", "displayname", "contactemail",
151 "teamdescription", "subscriptionpolicy",151 "teamdescription", "subscriptionpolicy",
152 "defaultmembershipperiod", "renewal_policy",152 "defaultmembershipperiod", "renewal_policy",
153 "defaultrenewalperiod", "teamowner",153 "defaultrenewalperiod", "teamowner",
154 ]154 ]
155 private_prefix = PRIVATE_TEAM_PREFIX155 private_prefix = PRIVATE_TEAM_PREFIX
156156
@@ -767,7 +767,7 @@
767767
768 def renderTable(self):768 def renderTable(self):
769 html = ['<table style="max-width: 80em">']769 html = ['<table style="max-width: 80em">']
770 items = self.subscribers.currentBatch()770 items = list(self.subscribers.currentBatch())
771 assert len(items) > 0, (771 assert len(items) > 0, (
772 "Don't call this method if there are no subscribers to show.")772 "Don't call this method if there are no subscribers to show.")
773 # When there are more than 10 items, we use multiple columns, but773 # When there are more than 10 items, we use multiple columns, but
774774
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-08-23 08:12:39 +0000
+++ lib/lp/registry/model/distroseries.py 2010-08-24 16:45:57 +0000
@@ -343,7 +343,7 @@
343 @cachedproperty343 @cachedproperty
344 def _all_packagings(self):344 def _all_packagings(self):
345 """Get an unordered list of all packagings.345 """Get an unordered list of all packagings.
346 346
347 :return: A ResultSet which can be decorated or tuned further. Use347 :return: A ResultSet which can be decorated or tuned further. Use
348 DistroSeries._packaging_row_to_packaging to extract the348 DistroSeries._packaging_row_to_packaging to extract the
349 packaging objects out.349 packaging objects out.
@@ -353,7 +353,7 @@
353 # Packaging object.353 # Packaging object.
354 # NB: precaching objects like this method tries to do has a very poor354 # NB: precaching objects like this method tries to do has a very poor
355 # hit rate with storm - many queries will still be executed; consider355 # hit rate with storm - many queries will still be executed; consider
356 # ripping this out and instead allowing explicit inclusion of things 356 # ripping this out and instead allowing explicit inclusion of things
357 # like Person._all_members does - returning a cached object graph.357 # like Person._all_members does - returning a cached object graph.
358 # -- RBC 20100810358 # -- RBC 20100810
359 # Avoid circular import failures.359 # Avoid circular import failures.
@@ -1810,11 +1810,7 @@
1810 DistroSeries.hide_all_translations == False,1810 DistroSeries.hide_all_translations == False,
1811 DistroSeries.id == POTemplate.distroseriesID)1811 DistroSeries.id == POTemplate.distroseriesID)
1812 result_set = result_set.config(distinct=True)1812 result_set = result_set.config(distinct=True)
1813 # XXX: henninge 2009-02-11 bug=217644: Convert to sequence right here1813 return result_set
1814 # because ResultSet reports a wrong count() when using DISTINCT. Also
1815 # ResultSet does not implement __len__(), which would make it more
1816 # like a sequence.
1817 return list(result_set)
18181814
1819 def findByName(self, name):1815 def findByName(self, name):
1820 """See `IDistroSeriesSet`."""1816 """See `IDistroSeriesSet`."""
18211817
=== modified file 'lib/lp/registry/model/mailinglist.py'
--- lib/lp/registry/model/mailinglist.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/mailinglist.py 2010-08-24 16:45:57 +0000
@@ -389,7 +389,7 @@
389 TeamParticipation.team == self.team,389 TeamParticipation.team == self.team,
390 MailingListSubscription.person == Person.id,390 MailingListSubscription.person == Person.id,
391 MailingListSubscription.mailing_list == self)391 MailingListSubscription.mailing_list == self)
392 return results.order_by(Person.displayname)392 return results.order_by(Person.displayname, Person.name)
393393
394 def subscribe(self, person, address=None):394 def subscribe(self, person, address=None):
395 """See `IMailingList`."""395 """See `IMailingList`."""
@@ -451,8 +451,9 @@
451 MailingListSubscription.personID451 MailingListSubscription.personID
452 == EmailAddress.personID),452 == EmailAddress.personID),
453 # pylint: disable-msg=C0301453 # pylint: disable-msg=C0301
454 LeftJoin(MailingList,454 LeftJoin(
455 MailingList.id == MailingListSubscription.mailing_listID),455 MailingList,
456 MailingList.id == MailingListSubscription.mailing_listID),
456 LeftJoin(TeamParticipation,457 LeftJoin(TeamParticipation,
457 TeamParticipation.personID458 TeamParticipation.personID
458 == MailingListSubscription.personID),459 == MailingListSubscription.personID),
@@ -472,8 +473,9 @@
472 MailingListSubscription.email_addressID473 MailingListSubscription.email_addressID
473 == EmailAddress.id),474 == EmailAddress.id),
474 # pylint: disable-msg=C0301475 # pylint: disable-msg=C0301
475 LeftJoin(MailingList,476 LeftJoin(
476 MailingList.id == MailingListSubscription.mailing_listID),477 MailingList,
478 MailingList.id == MailingListSubscription.mailing_listID),
477 LeftJoin(TeamParticipation,479 LeftJoin(TeamParticipation,
478 TeamParticipation.personID480 TeamParticipation.personID
479 == MailingListSubscription.personID),481 == MailingListSubscription.personID),
@@ -664,8 +666,9 @@
664 MailingListSubscription.personID666 MailingListSubscription.personID
665 == EmailAddress.personID),667 == EmailAddress.personID),
666 # pylint: disable-msg=C0301668 # pylint: disable-msg=C0301
667 LeftJoin(MailingList,669 LeftJoin(
668 MailingList.id == MailingListSubscription.mailing_listID),670 MailingList,
671 MailingList.id == MailingListSubscription.mailing_listID),
669 LeftJoin(TeamParticipation,672 LeftJoin(TeamParticipation,
670 TeamParticipation.personID673 TeamParticipation.personID
671 == MailingListSubscription.personID),674 == MailingListSubscription.personID),
@@ -678,8 +681,7 @@
678 team.id for team in store.find(681 team.id for team in store.find(
679 Person,682 Person,
680 And(Person.name.is_in(team_names),683 And(Person.name.is_in(team_names),
681 Person.teamowner != None))684 Person.teamowner != None)))
682 )
683 list_ids = set(685 list_ids = set(
684 mailing_list.id for mailing_list in store.find(686 mailing_list.id for mailing_list in store.find(
685 MailingList,687 MailingList,
@@ -709,8 +711,9 @@
709 MailingListSubscription.email_addressID711 MailingListSubscription.email_addressID
710 == EmailAddress.id),712 == EmailAddress.id),
711 # pylint: disable-msg=C0301713 # pylint: disable-msg=C0301
712 LeftJoin(MailingList,714 LeftJoin(
713 MailingList.id == MailingListSubscription.mailing_listID),715 MailingList,
716 MailingList.id == MailingListSubscription.mailing_listID),
714 LeftJoin(TeamParticipation,717 LeftJoin(TeamParticipation,
715 TeamParticipation.personID718 TeamParticipation.personID
716 == MailingListSubscription.personID),719 == MailingListSubscription.personID),
@@ -756,8 +759,7 @@
756 team.id for team in store.find(759 team.id for team in store.find(
757 Person,760 Person,
758 And(Person.name.is_in(team_names),761 And(Person.name.is_in(team_names),
759 Person.teamowner != None))762 Person.teamowner != None)))
760 )
761 team_members = store.using(*tables).find(763 team_members = store.using(*tables).find(
762 (Team.name, Person.displayname, EmailAddress.email),764 (Team.name, Person.displayname, EmailAddress.email),
763 And(TeamParticipation.teamID.is_in(team_ids),765 And(TeamParticipation.teamID.is_in(team_ids),
764766
=== modified file 'lib/lp/registry/tests/test_mailinglist.py'
--- lib/lp/registry/tests/test_mailinglist.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_mailinglist.py 2010-08-24 16:45:57 +0000
@@ -1,64 +1,64 @@
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
4from __future__ import with_statement
5
4__metaclass__ = type6__metaclass__ = type
5__all__ = []7__all__ = []
68
79from canonical.testing import DatabaseFunctionalLayer
8import unittest
9
10from canonical.launchpad.ftests import login
11from canonical.testing import LaunchpadFunctionalLayer
12from lp.registry.interfaces.mailinglistsubscription import (10from lp.registry.interfaces.mailinglistsubscription import (
13 MailingListAutoSubscribePolicy,11 MailingListAutoSubscribePolicy,
14 )12 )
15from lp.registry.interfaces.person import TeamSubscriptionPolicy13from lp.registry.interfaces.person import TeamSubscriptionPolicy
16from lp.testing import TestCaseWithFactory14from lp.testing import login_celebrity, person_logged_in, TestCaseWithFactory
1715
1816
19class MailingList_getSubscribers_TestCase(TestCaseWithFactory):17class MailingList_getSubscribers_TestCase(TestCaseWithFactory):
20 """Tests for `IMailingList`.getSubscribers()."""18 """Tests for `IMailingList`.getSubscribers()."""
2119
22 layer = LaunchpadFunctionalLayer20 layer = DatabaseFunctionalLayer
2321
24 def setUp(self):22 def setUp(self):
25 # Create a team (tied to a mailing list) with one former member, one
26 # pending member and one active member.
27 TestCaseWithFactory.setUp(self)23 TestCaseWithFactory.setUp(self)
28 login('foo.bar@canonical.com')24 self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
25 'test-mailinglist', 'team-owner')
26
27 def test_only_active_members_can_be_subscribers(self):
29 former_member = self.factory.makePerson()28 former_member = self.factory.makePerson()
30 pending_member = self.factory.makePerson()29 pending_member = self.factory.makePerson()
31 active_member = self.active_member = self.factory.makePerson()30 active_member = self.active_member = self.factory.makePerson()
32 self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
33 'test-mailinglist', 'team-owner')
34 self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
35
36 # Each of our members want to be subscribed to a team's mailing list31 # Each of our members want to be subscribed to a team's mailing list
37 # whenever they join the team.32 # whenever they join the team.
33 login_celebrity('admin')
38 former_member.mailing_list_auto_subscribe_policy = (34 former_member.mailing_list_auto_subscribe_policy = (
39 MailingListAutoSubscribePolicy.ALWAYS)35 MailingListAutoSubscribePolicy.ALWAYS)
40 active_member.mailing_list_auto_subscribe_policy = (36 active_member.mailing_list_auto_subscribe_policy = (
41 MailingListAutoSubscribePolicy.ALWAYS)37 MailingListAutoSubscribePolicy.ALWAYS)
42 pending_member.mailing_list_auto_subscribe_policy = (38 pending_member.mailing_list_auto_subscribe_policy = (
43 MailingListAutoSubscribePolicy.ALWAYS)39 MailingListAutoSubscribePolicy.ALWAYS)
4440 self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
45 pending_member.join(self.team)41 pending_member.join(self.team)
46 self.assertEqual(False, pending_member.inTeam(self.team))
47
48 self.team.addMember(former_member, reviewer=self.team.teamowner)42 self.team.addMember(former_member, reviewer=self.team.teamowner)
49 former_member.leave(self.team)43 former_member.leave(self.team)
50 self.assertEqual(False, former_member.inTeam(self.team))
51
52 self.team.addMember(active_member, reviewer=self.team.teamowner)44 self.team.addMember(active_member, reviewer=self.team.teamowner)
53 self.assertEqual(True, active_member.inTeam(self.team))
54
55 def test_only_active_members_can_be_subscribers(self):
56 # Even though our 3 members want to subscribe to the team's mailing45 # Even though our 3 members want to subscribe to the team's mailing
57 # list, only the active member is considered a subscriber.46 # list, only the active member is considered a subscriber.
58 subscribers = [self.active_member]47 self.assertEqual(
59 self.assertEqual(48 [active_member], list(self.mailing_list.getSubscribers()))
60 subscribers, list(self.mailing_list.getSubscribers()))49
6150 def test_getSubscribers_order(self):
6251 person_1 = self.factory.makePerson(name="pb1", displayname="Me")
63def test_suite():52 with person_logged_in(person_1):
64 return unittest.TestLoader().loadTestsFromName(__name__)53 person_1.mailing_list_auto_subscribe_policy = (
54 MailingListAutoSubscribePolicy.ALWAYS)
55 person_1.join(self.team)
56 person_2 = self.factory.makePerson(name="pa2", displayname="Me")
57 with person_logged_in(person_2):
58 person_2.mailing_list_auto_subscribe_policy = (
59 MailingListAutoSubscribePolicy.ALWAYS)
60 person_2.join(self.team)
61 subscribers = self.mailing_list.getSubscribers()
62 self.assertEqual(2, subscribers.count())
63 self.assertEqual(
64 ['pa2', 'pb1'], [person.name for person in subscribers])
6565
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-08-22 03:09:51 +0000
+++ lib/lp/registry/vocabularies.py 2010-08-24 16:45:57 +0000
@@ -95,9 +95,6 @@
95 SQLBase,95 SQLBase,
96 sqlvalues,96 sqlvalues,
97 )97 )
98from canonical.launchpad.components.decoratedresultset import (
99 DecoratedResultSet,
100 )
101from canonical.launchpad.database.account import Account98from canonical.launchpad.database.account import Account
102from canonical.launchpad.database.emailaddress import EmailAddress99from canonical.launchpad.database.emailaddress import EmailAddress
103from canonical.launchpad.database.stormsugar import StartsWith100from canonical.launchpad.database.stormsugar import StartsWith
@@ -648,10 +645,7 @@
648 else:645 else:
649 result.order_by(Person.displayname, Person.name)646 result.order_by(Person.displayname, Person.name)
650 result.config(limit=self.LIMIT)647 result.config(limit=self.LIMIT)
651 # XXX: BradCrittenden 2009-04-24 bug=217644: Wrap the results to648 return result
652 # ensure the .count() method works until the Storm bug is fixed and
653 # integrated.
654 return DecoratedResultSet(result)
655649
656 def search(self, text):650 def search(self, text):
657 """Return people/teams whose fti or email address match :text:."""651 """Return people/teams whose fti or email address match :text:."""
@@ -727,10 +721,7 @@
727 result.config(distinct=True)721 result.config(distinct=True)
728 result.order_by(Person.displayname, Person.name)722 result.order_by(Person.displayname, Person.name)
729 result.config(limit=self.LIMIT)723 result.config(limit=self.LIMIT)
730 # XXX: BradCrittenden 2009-04-24 bug=217644: Wrap the results to724 return result
731 # ensure the .count() method works until the Storm bug is fixed and
732 # integrated.
733 return DecoratedResultSet(result)
734725
735726
736class ValidPersonVocabulary(ValidPersonOrTeamVocabulary):727class ValidPersonVocabulary(ValidPersonOrTeamVocabulary):
737728
=== modified file 'lib/lp/soyuz/doc/package-diff.txt'
--- lib/lp/soyuz/doc/package-diff.txt 2010-05-13 12:04:56 +0000
+++ lib/lp/soyuz/doc/package-diff.txt 2010-08-24 16:45:57 +0000
@@ -451,12 +451,7 @@
451 >>> packagediff_set.getPendingDiffs().count()451 >>> packagediff_set.getPendingDiffs().count()
452 7452 7
453453
454XXX cprov 20070530: storm doesn't go well with limited count()s454 >>> packagediff_set.getPendingDiffs(limit=2).count()
455See bug #217644. For now we have to listify the results and used
456the list length.
457
458 >>> r = packagediff_set.getPendingDiffs(limit=2)
459 >>> len(list(r))
460 2455 2
461456
462All package diffs targeting a set of source package releases can also457All package diffs targeting a set of source package releases can also
463458
=== modified file 'lib/lp/soyuz/scripts/initialise_distroseries.py'
--- lib/lp/soyuz/scripts/initialise_distroseries.py 2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/scripts/initialise_distroseries.py 2010-08-24 16:45:57 +0000
@@ -25,8 +25,10 @@
25 ArchivePurpose,25 ArchivePurpose,
26 IArchiveSet,26 IArchiveSet,
27 )27 )
28from lp.soyuz.interfaces.packageset import IPackagesetSet
28from lp.soyuz.interfaces.queue import PackageUploadStatus29from lp.soyuz.interfaces.queue import PackageUploadStatus
29from lp.soyuz.model.packagecloner import clone_packages30from lp.soyuz.model.packagecloner import clone_packages
31from lp.soyuz.model.packageset import Packageset
3032
3133
32class InitialisationError(Exception):34class InitialisationError(Exception):
@@ -270,10 +272,28 @@
270272
271 def _copy_packagesets(self):273 def _copy_packagesets(self):
272 """Copy packagesets from the parent distroseries."""274 """Copy packagesets from the parent distroseries."""
273 self._store.execute("""275 packagesets = self._store.find(Packageset, distroseries=self.parent)
274 INSERT INTO Packageset276 parent_to_child = {}
275 (distroseries, owner, name, description, packagesetgroup)277 # Create the packagesets, and any archivepermissions
276 SELECT %s, %s, name, description, packagesetgroup278 for parent_ps in packagesets:
277 FROM Packageset WHERE distroseries = %s279 child_ps = getUtility(IPackagesetSet).new(
278 """ % sqlvalues(280 parent_ps.name, parent_ps.description,
279 self.distroseries, self.distroseries.owner, self.parent))281 self.distroseries.owner, distroseries=self.distroseries,
282 related_set=parent_ps)
283 self._store.execute("""
284 INSERT INTO Archivepermission
285 (person, permission, archive, packageset, explicit)
286 SELECT person, permission, %s, %s, explicit
287 FROM Archivepermission WHERE packageset = %s
288 """ % sqlvalues(
289 self.distroseries.main_archive, child_ps.id,
290 parent_ps.id))
291 parent_to_child[parent_ps] = child_ps
292 # Copy the relations between sets, and the contents
293 for old_series_ps, new_series_ps in parent_to_child.items():
294 old_series_sets = old_series_ps.setsIncluded(
295 direct_inclusion=True)
296 for old_series_child in old_series_sets:
297 new_series_ps.add(parent_to_child[old_series_child])
298 new_series_ps.add(old_series_ps.sourcesIncluded(
299 direct_inclusion=True))
280300
=== modified file 'lib/lp/soyuz/scripts/tests/test_initialise_distroseries.py'
--- lib/lp/soyuz/scripts/tests/test_initialise_distroseries.py 2010-08-20 20:31:18 +0000
+++ lib/lp/soyuz/scripts/tests/test_initialise_distroseries.py 2010-08-24 16:45:57 +0000
@@ -23,6 +23,7 @@
23from canonical.testing.layers import LaunchpadZopelessLayer23from canonical.testing.layers import LaunchpadZopelessLayer
24from lp.buildmaster.interfaces.buildbase import BuildStatus24from lp.buildmaster.interfaces.buildbase import BuildStatus
25from lp.registry.interfaces.pocket import PackagePublishingPocket25from lp.registry.interfaces.pocket import PackagePublishingPocket
26from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
26from lp.soyuz.interfaces.packageset import IPackagesetSet27from lp.soyuz.interfaces.packageset import IPackagesetSet
27from lp.soyuz.interfaces.sourcepackageformat import SourcePackageFormat28from lp.soyuz.interfaces.sourcepackageformat import SourcePackageFormat
28from lp.soyuz.model.distroarchseries import DistroArchSeries29from lp.soyuz.model.distroarchseries import DistroArchSeries
@@ -87,7 +88,7 @@
87 self.ubuntu['breezy-autotest'])88 self.ubuntu['breezy-autotest'])
88 ids = InitialiseDistroSeries(foobuntu)89 ids = InitialiseDistroSeries(foobuntu)
89 self.assertRaisesWithContent(90 self.assertRaisesWithContent(
90 InitialisationError,"Parent series queues are not empty.",91 InitialisationError, "Parent series queues are not empty.",
91 ids.check)92 ids.check)
9293
93 def assertDistroSeriesInitialisedCorrectly(self, foobuntu):94 def assertDistroSeriesInitialisedCorrectly(self, foobuntu):
@@ -191,6 +192,7 @@
191192
192 def test_copying_packagesets(self):193 def test_copying_packagesets(self):
193 # If a parent series has packagesets, we should copy them194 # If a parent series has packagesets, we should copy them
195 uploader = self.factory.makePerson()
194 test1 = getUtility(IPackagesetSet).new(196 test1 = getUtility(IPackagesetSet).new(
195 u'test1', u'test 1 packageset', self.hoary.owner,197 u'test1', u'test 1 packageset', self.hoary.owner,
196 distroseries=self.hoary)198 distroseries=self.hoary)
@@ -199,13 +201,11 @@
199 distroseries=self.hoary)201 distroseries=self.hoary)
200 test3 = getUtility(IPackagesetSet).new(202 test3 = getUtility(IPackagesetSet).new(
201 u'test3', u'test 3 packageset', self.hoary.owner,203 u'test3', u'test 3 packageset', self.hoary.owner,
202 distroseries=self.hoary)204 distroseries=self.hoary, related_set=test2)
203 foobuntu = self._create_distroseries(self.hoary)205 test1.addSources('pmount')
204 self._set_pending_to_failed(self.hoary)206 getUtility(IArchivePermissionSet).newPackagesetUploader(
205 transaction.commit()207 self.hoary.main_archive, uploader, test1)
206 ids = InitialiseDistroSeries(foobuntu)208 foobuntu = self._full_initialise()
207 ids.check()
208 ids.initialise()
209 # We can fetch the copied sets from foobuntu209 # We can fetch the copied sets from foobuntu
210 foobuntu_test1 = getUtility(IPackagesetSet).getByName(210 foobuntu_test1 = getUtility(IPackagesetSet).getByName(
211 u'test1', distroseries=foobuntu)211 u'test1', distroseries=foobuntu)
@@ -219,8 +219,26 @@
219 self.assertEqual(test2.description, foobuntu_test2.description)219 self.assertEqual(test2.description, foobuntu_test2.description)
220 self.assertEqual(test3.description, foobuntu_test3.description)220 self.assertEqual(test3.description, foobuntu_test3.description)
221 self.assertEqual(foobuntu_test1.relatedSets().one(), test1)221 self.assertEqual(foobuntu_test1.relatedSets().one(), test1)
222 self.assertEqual(foobuntu_test2.relatedSets().one(), test2)222 self.assertEqual(
223 self.assertEqual(foobuntu_test3.relatedSets().one(), test3)223 list(foobuntu_test2.relatedSets()),
224 [test2, test3, foobuntu_test3])
225 self.assertEqual(
226 list(foobuntu_test3.relatedSets()),
227 [test2, foobuntu_test2, test3])
228 # The contents of the packagesets will have been copied.
229 foobuntu_srcs = foobuntu_test1.getSourcesIncluded(
230 direct_inclusion=True)
231 hoary_srcs = test1.getSourcesIncluded(direct_inclusion=True)
232 self.assertEqual(foobuntu_srcs, hoary_srcs)
233 # The uploader can also upload to the new distroseries.
234 self.assertTrue(
235 getUtility(IArchivePermissionSet).isSourceUploadAllowed(
236 self.hoary.main_archive, 'pmount', uploader,
237 distroseries=self.hoary))
238 self.assertTrue(
239 getUtility(IArchivePermissionSet).isSourceUploadAllowed(
240 foobuntu.main_archive, 'pmount', uploader,
241 distroseries=foobuntu))
224242
225 def test_script(self):243 def test_script(self):
226 # Do an end-to-end test using the command-line tool244 # Do an end-to-end test using the command-line tool
227245
=== modified file 'lib/lp/testing/fakelibrarian.py'
--- lib/lp/testing/fakelibrarian.py 2010-08-20 20:31:18 +0000
+++ lib/lp/testing/fakelibrarian.py 2010-08-24 16:45:57 +0000
@@ -151,6 +151,19 @@
151 alias.checkCommitted()151 alias.checkCommitted()
152 return StringIO(alias.content_string)152 return StringIO(alias.content_string)
153153
154 def pretendCommit(self):
155 """Pretend that there's been a commit.
156
157 When you add a file to the librarian (real or fake), it is not
158 fully available until the transaction that added the file has
159 been committed. Call this method to make the FakeLibrarian act
160 as if there's been a commit, without actually committing a
161 database transaction.
162 """
163 # Note that all files have been committed to storage.
164 for alias in self.aliases.itervalues():
165 alias.file_committed = True
166
154 def _makeAlias(self, file_id, name, content, content_type):167 def _makeAlias(self, file_id, name, content, content_type):
155 """Create a `LibraryFileAlias`."""168 """Create a `LibraryFileAlias`."""
156 alias = InstrumentedLibraryFileAlias(169 alias = InstrumentedLibraryFileAlias(
@@ -195,9 +208,7 @@
195208
196 def afterCompletion(self, txn):209 def afterCompletion(self, txn):
197 """See `ISynchronizer`."""210 """See `ISynchronizer`."""
198 # Note that all files have been committed to storage.211 self.pretendCommit()
199 for alias in self.aliases.itervalues():
200 alias.file_committed = True
201212
202 def newTransaction(self, txn):213 def newTransaction(self, txn):
203 """See `ISynchronizer`."""214 """See `ISynchronizer`."""
204215
=== modified file 'lib/lp/testing/tests/test_fakelibrarian.py'
--- lib/lp/testing/tests/test_fakelibrarian.py 2010-08-20 20:31:18 +0000
+++ lib/lp/testing/tests/test_fakelibrarian.py 2010-08-24 16:45:57 +0000
@@ -109,6 +109,15 @@
109 self.assertTrue(verifyObject(ISynchronizer, self.fake_librarian))109 self.assertTrue(verifyObject(ISynchronizer, self.fake_librarian))
110 self.assertIsInstance(self.fake_librarian, FakeLibrarian)110 self.assertIsInstance(self.fake_librarian, FakeLibrarian)
111111
112 def test_pretend_commit(self):
113 name, text, alias_id = self._storeFile()
114
115 self.fake_librarian.pretendCommit()
116
117 retrieved_alias = getUtility(ILibraryFileAliasSet)[alias_id]
118 retrieved_alias.open()
119 self.assertEqual(text, retrieved_alias.read())
120
112121
113class TestRealLibrarian(LibraryAccessScenarioMixin, TestCaseWithFactory):122class TestRealLibrarian(LibraryAccessScenarioMixin, TestCaseWithFactory):
114 """Test the supported interface subset on the real librarian."""123 """Test the supported interface subset on the real librarian."""
115124
=== modified file 'versions.cfg'
--- versions.cfg 2010-08-18 19:41:20 +0000
+++ versions.cfg 2010-08-24 16:45:57 +0000
@@ -101,7 +101,7 @@
101z3c.ptcompat = 0.5.3101z3c.ptcompat = 0.5.3
102z3c.recipe.filetemplate = 2.1.0102z3c.recipe.filetemplate = 2.1.0
103z3c.recipe.i18n = 0.5.3103z3c.recipe.i18n = 0.5.3
104z3c.recipe.scripts = 1.0.0dev-gary-r110068104z3c.recipe.scripts = 1.0.0
105z3c.recipe.tag = 0.2.0105z3c.recipe.tag = 0.2.0
106z3c.rml = 0.7.3106z3c.rml = 0.7.3
107z3c.skin.pagelet = 1.0.2107z3c.skin.pagelet = 1.0.2
@@ -111,12 +111,12 @@
111z3c.viewlet = 1.0.0111z3c.viewlet = 1.0.0
112z3c.viewtemplate = 0.3.2112z3c.viewtemplate = 0.3.2
113z3c.zrtresource = 1.0.1113z3c.zrtresource = 1.0.1
114zc.buildout = 1.5.0dev-gary-r111190114zc.buildout = 1.5.0
115zc.catalog = 1.2.0115zc.catalog = 1.2.0
116zc.datetimewidget = 0.5.2116zc.datetimewidget = 0.5.2
117zc.i18n = 0.5.2117zc.i18n = 0.5.2
118zc.lockfile = 1.0.0118zc.lockfile = 1.0.0
119zc.recipe.egg = 1.2.3dev-gary-r110068119zc.recipe.egg = 1.3.0
120zc.zservertracelog = 1.1.5120zc.zservertracelog = 1.1.5
121ZConfig = 2.7.1121ZConfig = 2.7.1
122zdaemon = 2.0.4122zdaemon = 2.0.4

Subscribers

People subscribed via source and target branches

to status/vote changes: