launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #01684
[Merge] lp:~lamont/launchpad/lp-buildd-72 into lp:launchpad
LaMont Jones has proposed merging lp:~lamont/launchpad/lp-buildd-72 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
launchpad-buildd rev 72 as released
--
https://code.launchpad.net/~lamont/launchpad/lp-buildd-72/+merge/39368
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lamont/launchpad/lp-buildd-72 into lp:launchpad.
=== modified file 'Makefile'
=== modified file 'daemons/librarian.tac'
=== modified file 'database/schema/security.cfg'
=== modified file 'lib/canonical/buildd/debian/changelog'
--- lib/canonical/buildd/debian/changelog 2010-10-19 20:00:06 +0000
+++ lib/canonical/buildd/debian/changelog 2010-10-26 13:35:01 +0000
@@ -1,10 +1,26 @@
-launchpad-buildd (71) hardy-cat; urgency=low
-
- * Detect ppa hosts for build recipes. LP#662664
- * Better recipe builds. LP#599100, 627119, 479705
-
- -- LaMont Jones <lamont@xxxxxxxxxxxxx> Tue, 19 Oct 2010 13:48:33 -0600
-
+<<<<<<< TREE
+launchpad-buildd (71) hardy-cat; urgency=low
+
+ * Detect ppa hosts for build recipes. LP#662664
+ * Better recipe builds. LP#599100, 627119, 479705
+
+ -- LaMont Jones <lamont@xxxxxxxxxxxxx> Tue, 19 Oct 2010 13:48:33 -0600
+
+=======
+launchpad-buildd (72) hardy-cat; urgency=low
+
+ * break out readyservice.py from tachandler.py. LP#663828
+
+ -- LaMont Jones <lamont@xxxxxxxxxxxxx> Wed, 20 Oct 2010 13:03:23 -0600
+
+launchpad-buildd (71) hardy-cat; urgency=low
+
+ * Detect ppa hosts for build recipes. LP#662664
+ * Better recipe builds. LP#599100, 627119, 479705
+
+ -- LaMont Jones <lamont@xxxxxxxxxxxxx> Tue, 19 Oct 2010 13:48:33 -0600
+
+>>>>>>> MERGE-SOURCE
launchpad-buildd (70) hardy-cat; urgency=low
[ LaMont Jones ]
=== modified file 'lib/canonical/config/schema-lazr.conf'
=== modified file 'lib/canonical/launchpad/browser/oauth.py'
--- lib/canonical/launchpad/browser/oauth.py 2010-10-20 20:31:30 +0000
+++ lib/canonical/launchpad/browser/oauth.py 2010-10-26 13:35:01 +0000
@@ -112,6 +112,7 @@
return form.token is not None and not form.token.is_reviewed
+<<<<<<< TREE
def token_review_success(form, action, data):
"""The success callback for a button to approve a token."""
form.reviewToken(action.permission, action.duration)
@@ -131,7 +132,15 @@
}
+=======
+def token_review_success(form, action, data):
+ """The success callback for a button to approve a token."""
+ form.reviewToken(action.permission)
+
+
+>>>>>>> MERGE-SOURCE
def create_oauth_permission_actions():
+<<<<<<< TREE
"""Return two `Actions` objects containing each possible `OAuthPermission`.
The first `Actions` object contains every action supported by the
@@ -142,12 +151,23 @@
all_actions = Actions()
ordinary_actions = Actions()
desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
+=======
+ """Return two `Actions` objects containing each possible `OAuthPermission`.
+
+ The first `Actions` object contains every action supported by the
+ OAuthAuthorizeTokenView. The second list contains a good default
+ set of actions, omitting special permissions like DESKTOP_INTEGRATION.
+ """
+ all_actions = Actions()
+ ordinary_actions = Actions()
+>>>>>>> MERGE-SOURCE
for permission in OAuthPermission.items:
action = Action(
permission.title, name=permission.name,
success=token_review_success,
condition=token_exists_and_is_not_reviewed)
action.permission = permission
+<<<<<<< TREE
action.duration = None
all_actions.append(action)
if permission != desktop_permission:
@@ -169,6 +189,13 @@
return all_actions, ordinary_actions
+=======
+ all_actions.append(action)
+ if permission != OAuthPermission.DESKTOP_INTEGRATION:
+ ordinary_actions.append(action)
+ return all_actions, ordinary_actions
+
+>>>>>>> MERGE-SOURCE
class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
"""Where users authorize consumers to access Launchpad on their behalf."""
@@ -214,12 +241,20 @@
# DESKTOP_INTEGRATION cannot be requested as one of several
# options--it must be the only option (other than
+<<<<<<< TREE
# UNAUTHORIZED). If there is any item in the list that doesn't
# use DESKTOP_INTEGRATION, remove it from the list.
desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
if (desktop_permission.name in allowed_permissions
+=======
+ # UNAUTHORIZED). If DESKTOP_INTEGRATION is one of several
+ # options, remove it from the list.
+ desktop_permission = OAuthPermission.DESKTOP_INTEGRATION
+ if (desktop_permission.name in allowed_permissions
+>>>>>>> MERGE-SOURCE
and len(allowed_permissions) > 1):
+<<<<<<< TREE
allowed_permissions.remove(desktop_permission.name)
if desktop_permission.name in allowed_permissions:
@@ -273,6 +308,54 @@
if (action.permission.name in allowed_permissions
or action.permission is OAuthPermission.UNAUTHORIZED):
actions.append(action)
+=======
+ allowed_permissions.remove(desktop_permission.name)
+
+ if desktop_permission.name in allowed_permissions:
+ if not self.token.consumer.is_integrated_desktop:
+ # Consumers may only ask for desktop integration if
+ # they give a desktop type (eg. "Ubuntu") and a
+ # user-recognizable desktop name (eg. the hostname).
+ raise Unauthorized(
+ ('Consumer "%s" asked for desktop integration, '
+ "but didn't say what kind of desktop it is, or name "
+ "the computer being integrated."
+ % self.token.consumer.key))
+
+ # We're going for desktop integration. The only two
+ # possibilities are "allow" and "deny". We'll customize
+ # the "allow" message using the hostname provided by the
+ # desktop.
+ #
+ # Since self.actions is a descriptor that returns copies
+ # of Action objects, we can modify the actions we get
+ # in-place without ruining the Action objects for everyone
+ # else.
+ desktop_name = self.token.consumer.integrated_desktop_name
+ label = (
+ 'Give all programs running on "%s" access '
+ 'to my Launchpad account.')
+ allow_action = [
+ action for action in self.actions
+ if action.name == desktop_permission.name][0]
+ allow_action.label = label % desktop_name
+ actions.append(allow_action)
+
+ # We'll customize the "deny" message as well.
+ label = "No, thanks, I don't trust "%s"."
+ deny_action = [
+ action for action in self.actions
+ if action.name == OAuthPermission.UNAUTHORIZED.name][0]
+ deny_action.label = label % desktop_name
+ actions.append(deny_action)
+
+ else:
+ # We're going for web-based integration.
+ for action in self.actions_excluding_special_permissions:
+ if (action.permission.name in allowed_permissions
+ or action.permission is OAuthPermission.UNAUTHORIZED):
+ actions.append(action)
+>>>>>>> MERGE-SOURCE
if len(list(actions)) == 1:
# The only visible action is UNAUTHORIZED. That means the
@@ -281,6 +364,7 @@
# UNAUTHORIZED). Rather than present the end-user with an
# impossible situation where their only option is to deny
# access, we'll present the full range of actions (except
+<<<<<<< TREE
# for special permissions like DESKTOP_INTEGRATION).
return self.actions_excluding_special_permissions
return actions
@@ -303,6 +387,11 @@
raise AssertionError(
"UNAUTHORIZED permission level should always be visible, "
"but wasn't.")
+=======
+ # for special permissions like DESKTOP_INTEGRATION).
+ return self.actions_excluding_special_permissions
+ return actions
+>>>>>>> MERGE-SOURCE
def initialize(self):
self.storeTokenContext()
@@ -310,30 +399,58 @@
key = form.get('oauth_token')
if key:
self.token = getUtility(IOAuthRequestTokenSet).getByKey(key)
-
- callback = self.request.form.get('oauth_callback')
- if (self.token is not None
- and self.token.consumer.is_integrated_desktop):
- # Nip problems in the bud by appling special rules about
- # what desktop integrations are allowed to do.
- if callback is not None:
- # A desktop integration is not allowed to specify a callback.
- raise Unauthorized(
- "A desktop integration may not specify an "
- "OAuth callback URL.")
- # A desktop integration token can only have one of two
- # permission levels: "Desktop Integration" and
- # "Unauthorized". It shouldn't even be able to ask for any
- # other level.
- for action in self.visible_actions:
- if action.permission not in (
- OAuthPermission.DESKTOP_INTEGRATION,
- OAuthPermission.UNAUTHORIZED):
- raise Unauthorized(
- ("Desktop integration token requested a permission "
- '("%s") not supported for desktop-wide use.')
- % action.label)
-
+<<<<<<< TREE
+
+ callback = self.request.form.get('oauth_callback')
+ if (self.token is not None
+ and self.token.consumer.is_integrated_desktop):
+ # Nip problems in the bud by appling special rules about
+ # what desktop integrations are allowed to do.
+ if callback is not None:
+ # A desktop integration is not allowed to specify a callback.
+ raise Unauthorized(
+ "A desktop integration may not specify an "
+ "OAuth callback URL.")
+ # A desktop integration token can only have one of two
+ # permission levels: "Desktop Integration" and
+ # "Unauthorized". It shouldn't even be able to ask for any
+ # other level.
+ for action in self.visible_actions:
+ if action.permission not in (
+ OAuthPermission.DESKTOP_INTEGRATION,
+ OAuthPermission.UNAUTHORIZED):
+ raise Unauthorized(
+ ("Desktop integration token requested a permission "
+ '("%s") not supported for desktop-wide use.')
+ % action.label)
+
+=======
+
+
+ callback = self.request.form.get('oauth_callback')
+ if (self.token is not None
+ and self.token.consumer.is_integrated_desktop):
+ # Nip problems in the bud by appling special rules about
+ # what desktop integrations are allowed to do.
+ if callback is not None:
+ # A desktop integration is not allowed to specify a callback.
+ raise Unauthorized(
+ "A desktop integration may not specify an "
+ "OAuth callback URL.")
+ # A desktop integration token can only have one of two
+ # permission levels: "Desktop Integration" and
+ # "Unauthorized". It shouldn't even be able to ask for any
+ # other level.
+ for action in self.visible_actions:
+ if action.permission not in (
+ OAuthPermission.DESKTOP_INTEGRATION,
+ OAuthPermission.UNAUTHORIZED):
+ raise Unauthorized(
+ ("Desktop integration token requested a permission "
+ '("%s") not supported for desktop-wide use.')
+ % action.label)
+
+>>>>>>> MERGE-SOURCE
super(OAuthAuthorizeTokenView, self).initialize()
def render(self):
=== modified file 'lib/canonical/launchpad/database/oauth.py'
--- lib/canonical/launchpad/database/oauth.py 2010-10-18 12:47:22 +0000
+++ lib/canonical/launchpad/database/oauth.py 2010-10-26 13:35:01 +0000
@@ -304,6 +304,7 @@
else:
return None
+<<<<<<< TREE
@property
def is_expired(self):
now = datetime.now(pytz.timezone('UTC'))
@@ -311,6 +312,15 @@
return expires <= now
def review(self, user, permission, context=None, date_expires=None):
+=======
+ @property
+ def is_expired(self):
+ now = datetime.now(pytz.timezone('UTC'))
+ expires = self.date_created + timedelta(hours=REQUEST_TOKEN_VALIDITY)
+ return expires <= now
+
+ def review(self, user, permission, context=None):
+>>>>>>> MERGE-SOURCE
"""See `IOAuthRequestToken`."""
if self.is_reviewed:
raise AssertionError(
=== modified file 'lib/canonical/launchpad/doc/oauth.txt'
--- lib/canonical/launchpad/doc/oauth.txt 2010-10-21 19:03:50 +0000
+++ lib/canonical/launchpad/doc/oauth.txt 2010-10-26 13:35:01 +0000
@@ -1,9 +1,343 @@
+<<<<<<< TREE
= OAuth =
Most of the OAuth doctests have been converted into unit tests and
moved to test_oauth_tokens.py
== Nonces and timestamps ==
+=======
+=====
+OAuth
+=====
+
+This is a mechanism for allowing a third party application to access
+Launchpad on a user's behalf. These applications are identified by a
+unique key and are stored as OAuthConsumers. The OAuth specification is
+defined in <http://oauth.net/core/1.0/>.
+
+These applications (also called consumers) are managed by the
+OAuthConsumerSet utility.
+
+ >>> from canonical.launchpad.webapp.testing import verifyObject
+ >>> from canonical.launchpad.webapp.interfaces import (
+ ... AccessLevel, OAuthPermission)
+ >>> from canonical.launchpad.interfaces import (
+ ... IOAuthAccessToken, IOAuthConsumer, IOAuthConsumerSet,
+ ... IOAuthNonce, IOAuthRequestToken, IPersonSet)
+ >>> consumer_set = getUtility(IOAuthConsumerSet)
+ >>> verifyObject(IOAuthConsumerSet, consumer_set)
+ True
+
+ >>> consumer = consumer_set.new(key='asdfg')
+ >>> verifyObject(IOAuthConsumer, consumer)
+ True
+
+ >>> consumer_set.getByKey('asdfg') == consumer
+ True
+
+ >>> print consumer_set.getByKey('gfdsa')
+ None
+
+As mentioned above, the keys are unique, so we can't create a second
+Consumer with the same key.
+
+ >>> consumer_set.new(key='asdfg')
+ Traceback (most recent call last):
+ ...
+ AssertionError: ...
+
+Desktop consumers
+=================
+
+In a web context, each application is represented by a unique consumer
+key. But a typical user sitting at a typical desktop (or other
+personal computer), using multiple desktop applications that integrate
+with Launchpad, is represented by a single consumer key. The user's
+session as a whole is a single "consumer", and the consumer key is
+expected to contain structured information: the type of system
+(usually the operating system plus the word "desktop") and a string
+that the end-user would recognize as identifying their computer.
+
+ >>> desktop_key = consumer_set.new(
+ ... "System-wide: Ubuntu desktop (hostname)")
+ >>> desktop_key.is_integrated_desktop
+ True
+ >>> print desktop_key.integrated_desktop_type
+ Ubuntu desktop
+ >>> print desktop_key.integrated_desktop_name
+ hostname
+
+ >>> desktop_key = consumer_set.new(
+ ... "System-wide: Android phone (My Phone)")
+ >>> desktop_key.is_integrated_desktop
+ True
+ >>> print desktop_key.integrated_desktop_type
+ Android phone
+ >>> print desktop_key.integrated_desktop_name
+ My Phone
+
+A normal OAuth consumer does not have this information.
+
+ >>> ordinary_key = consumer_set.new("Not a desktop at all.")
+ >>> ordinary_key.is_integrated_desktop
+ False
+ >>> print ordinary_key.integrated_desktop_type
+ None
+ >>> print ordinary_key.integrated_desktop_name
+ None
+
+Request tokens
+==============
+
+When a consumer wants to access protected resources on Launchpad, it
+must first ask for an OAuthRequestToken, which is then used when the
+consumer sends the user to the Launchpad authorization page.
+
+
+Creating request tokens
+-----------------------
+
+The request tokens are created using IOAuthConsumer.newRequestToken().
+
+ # XXX EdwinGrubbs 2008-10-03 bug=277756
+ # Tests could be simplified with helper methods for creating tokens
+ # in different states.
+ >>> request_token = consumer.newRequestToken()
+ >>> verifyObject(IOAuthRequestToken, request_token)
+ True
+
+The token's key and secret have a length of 20 and 80 respectively.
+
+ >>> len(request_token.key)
+ 20
+ >>> len(request_token.secret)
+ 80
+
+Newly created tokens have no context associated with.
+
+ >>> print request_token.context
+ None
+
+Initially, a token does not have a person or permission associated with it as
+the consumer doesn't know the user's identity on Launchpad.
+
+ >>> print request_token.person
+ None
+ >>> print request_token.permission
+ None
+ >>> print request_token.date_reviewed
+ None
+
+Once the user reviews (approve/decline) the consumer's request, the
+token is considered used and can only be exchanged for an access token
+(when the access is granted by the user).
+
+ >>> salgado = getUtility(IPersonSet).getByName('salgado')
+ >>> request_token.review(salgado, OAuthPermission.WRITE_PUBLIC)
+ >>> from canonical.launchpad.ftests import syncUpdate
+ >>> syncUpdate(request_token)
+
+ >>> from datetime import datetime, timedelta
+ >>> import pytz
+ >>> print request_token.person.name
+ salgado
+ >>> request_token.permission
+ <DBItem OAuthPermission.WRITE_PUBLIC...
+ >>> request_token.date_reviewed <= datetime.now(pytz.timezone('UTC'))
+ True
+ >>> request_token.is_reviewed
+ True
+
+When reviewing a token, we can also change the context associated with
+it, which means the consumer using that token will only have access
+to things linked to that context (Product, ProjectGroup, Distribution,
+DistroSourcePackage).
+
+ >>> from lp.registry.interfaces.distribution import IDistributionSet
+ >>> from lp.registry.interfaces.product import IProductSet
+ >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
+
+ >>> firefox = getUtility(IProductSet)['firefox']
+ >>> request_token2 = consumer.newRequestToken()
+ >>> request_token2.review(
+ ... salgado, OAuthPermission.WRITE_PRIVATE, context=firefox)
+ >>> print request_token2.context.title
+ Mozilla Firefox
+
+ >>> mozilla = getUtility(IProjectGroupSet)['mozilla']
+ >>> request_token2 = consumer.newRequestToken()
+ >>> request_token2.review(
+ ... salgado, OAuthPermission.WRITE_PRIVATE, context=mozilla)
+ >>> print request_token2.context.title
+ The Mozilla Project
+
+ >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
+ >>> evolution = ubuntu.getSourcePackage('evolution')
+ >>> request_token2 = consumer.newRequestToken()
+ >>> request_token2.review(
+ ... salgado, OAuthPermission.WRITE_PRIVATE, context=evolution)
+
+ >>> from canonical.encoding import ascii_smash
+ >>> print ascii_smash(request_token2.context.title)
+ evolution package in Ubuntu
+
+
+Retrieving request tokens
+-------------------------
+
+Any consumer can retrieve its request tokens as long as it knows their
+keys.
+
+ >>> consumer.getRequestToken(request_token.key) == request_token
+ True
+
+If there is no token with the given key, or the existing token is
+associated with another consumer, getRequestToken() will return None.
+
+ >>> print consumer.getRequestToken('zzzzzzzz')
+ None
+ >>> consumer2 = consumer_set.new(key='foobar')
+ >>> print consumer2.getRequestToken(request_token.key)
+ None
+
+We also have OAuthRequestTokenSet.getByKey(), which allows us to get a
+request token with the given key regardless of the consumer associated
+with it.
+
+ >>> from canonical.launchpad.interfaces.oauth import IOAuthRequestTokenSet
+ >>> token_set = getUtility(IOAuthRequestTokenSet)
+ >>> token_set.getByKey(request_token.key) == request_token
+ True
+
+ >>> request_token2 = consumer2.newRequestToken()
+ >>> token_set.getByKey(request_token2.key) == request_token2
+ True
+
+ >>> print token_set.getByKey('zzzzzzzzz')
+ None
+
+
+Exchanging request tokens for access tokens
+-------------------------------------------
+
+Once a request token has been reviewed it may be exchanged for an access
+token. That may happen only if the user actually granted some sort of
+permission to the consumer when reviewing the request.
+
+The access token's permission will be the same as the request token's
+one, but it is an item of AccessLevel rather than OAuthPermission
+because the former doesn't have an UNAUTHORIZED item (which doesn't
+make sense in access tokens).
+
+ >>> request_token.is_reviewed
+ True
+ >>> request_token.permission
+ <DBItem OAuthPermission.WRITE_PUBLIC...
+ >>> access_token = request_token.createAccessToken()
+ >>> verifyObject(IOAuthAccessToken, access_token)
+ True
+ >>> access_token.permission
+ <DBItem AccessLevel.WRITE_PUBLIC...
+
+After the access token is generated, the request token is deleted.
+
+ >>> print consumer.getRequestToken(request_token.key)
+ None
+
+By default, access tokens don't expire.
+
+ >>> print access_token.date_expires
+ None
+
+Access tokens will also inherit the context from the request token.
+
+ >>> request_token2 = consumer.newRequestToken()
+ >>> request_token2.review(
+ ... salgado, OAuthPermission.WRITE_PRIVATE, context=firefox)
+ >>> access_token2 = request_token2.createAccessToken()
+ >>> print access_token2.context.title
+ Mozilla Firefox
+
+If the request token hasn't been reviewed yet, it can't be used to
+create an access token.
+
+ >>> request_token = consumer.newRequestToken()
+ >>> request_token.is_reviewed
+ False
+ >>> access_token = request_token.createAccessToken()
+ Traceback (most recent call last):
+ ...
+ AssertionError: ...
+
+The same holds true for request tokens that have UNAUTHORIZED as their
+permission.
+
+ >>> request_token.review(salgado, OAuthPermission.UNAUTHORIZED)
+ >>> request_token.is_reviewed
+ True
+ >>> access_token = request_token.createAccessToken()
+ Traceback (most recent call last):
+ ...
+ AssertionError: ...
+
+
+Access tokens
+=============
+
+As shown above, access tokens can be created from any reviewed (and
+authorized) request tokens. These tokens are then stored by the consumer
+and included in all further requests made on behalf of the same user, so
+we need a way to retrieve an access token from any consumer.
+
+ >>> consumer.getAccessToken(access_token.key) == access_token
+ True
+
+An access token can only be changed by the person associated with it.
+
+ >>> access_token.permission = OAuthPermission.WRITE_PUBLIC
+ Traceback (most recent call last):
+ ...
+ Unauthorized:...
+ >>> login_person(access_token.person)
+ >>> access_token.permission = AccessLevel.WRITE_PUBLIC
+
+From any given person it's possible to retrieve his non-expired access
+tokens.
+
+ >>> access_token.person.oauth_access_tokens.count()
+ 4
+ >>> access_token.date_expires = (
+ ... datetime.now(pytz.timezone('UTC')) - timedelta(hours=1))
+ >>> syncUpdate(access_token)
+ >>> access_token.person.oauth_access_tokens.count()
+ 3
+
+It's also possible to retrieve the user's non-expired request tokens.
+
+ >>> unclaimed_request_token = consumer.newRequestToken()
+ >>> unclaimed_request_token.review(salgado, OAuthPermission.WRITE_PUBLIC)
+ >>> salgado.oauth_request_tokens.count()
+ 5
+ >>> salgado.oauth_request_tokens[0].date_expires = (
+ ... datetime.now(pytz.timezone('UTC')) - timedelta(hours=1))
+ >>> syncUpdate(unclaimed_request_token)
+ >>> salgado.oauth_request_tokens.count()
+ 4
+
+A user has edit permission over his own access tokens, he can expire them.
+
+ >>> api_user = factory.makePerson()
+ >>> login_person(api_user)
+ >>> api_request_token = consumer.newRequestToken()
+ >>> api_request_token.review(api_user, OAuthPermission.WRITE_PUBLIC)
+ >>> api_access_token = api_request_token.createAccessToken()
+ >>> api_access_token.date_expires = (
+ ... datetime.now(pytz.timezone('UTC')) - timedelta(hours=1))
+
+
+Nonces and timestamps
+=====================
+>>>>>>> MERGE-SOURCE
A nonce is a random string, generated by the client for each request.
=== modified file 'lib/canonical/launchpad/doc/webapp-authorization.txt'
=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-21 10:30:20 +0000
+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-10-26 13:35:01 +0000
@@ -78,10 +78,15 @@
that isn't enough for the application. The user always has the option
to deny permission altogether.
+<<<<<<< TREE
>>> def authorize_token_browser(allow_permission):
+=======
+ >>> def authorize_token_main_content(allow_permission):
+>>>>>>> MERGE-SOURCE
... browser.open(
... "http://launchpad.dev/+authorize-token?%s&%s"
... % (urlencode(params), allow_permission))
+<<<<<<< TREE
>>> def authorize_token_main_content(allow_permission):
... authorize_token_browser(allow_permission)
@@ -92,6 +97,15 @@
... print_access_levels(main_content)
>>> print_access_levels_for(
+=======
+ ... return find_tag_by_id(browser.contents, 'maincontent')
+
+ >>> def print_access_levels_for(allow_permission):
+ ... main_content = authorize_token_main_content(allow_permission)
+ ... print_access_levels(main_content)
+
+ >>> print_access_levels_for(
+>>>>>>> MERGE-SOURCE
... 'allow_permission=WRITE_PUBLIC&allow_permission=WRITE_PRIVATE')
No Access
Change Non-Private Data
@@ -264,6 +278,7 @@
This request for accessing Launchpad on your behalf has been
reviewed ... ago.
See all applications authorized to access Launchpad on your behalf.
+<<<<<<< TREE
Desktop integration
===================
@@ -488,3 +503,97 @@
True
>>> print token.permission.name
UNAUTHORIZED
+=======
+
+Desktop integration
+===================
+
+The test case given above shows how to integrate a single application
+or website into Launchpad. But it's also possible to integrate an
+entire desktop environment into Launchpad.
+
+The desktop integration option is only available for OAuth consumers
+that say what kind of desktop they are (eg. Ubuntu) and give a name
+that a user can identify with their computer (eg. the hostname). Here,
+we'll create such a token.
+
+ >>> login('salgado@xxxxxxxxxx')
+ >>> desktop_key = "System-wide: Ubuntu desktop (mycomputer)"
+ >>> consumer = getUtility(IOAuthConsumerSet).new(desktop_key)
+ >>> token = consumer.newRequestToken()
+ >>> logout()
+
+When a desktop tries to integrate with Launchpad, the user gets a
+special warning about giving access to every program running on their
+desktop.
+
+ >>> params = dict(oauth_token=token.key)
+ >>> print extract_text(
+ ... authorize_token_main_content(
+ ... 'allow_permission=DESKTOP_INTEGRATION'))
+ Integrating mycomputer into your Launchpad account
+ The Ubuntu desktop called mycomputer wants access to your
+ Launchpad account. If you allow the integration, all applications
+ running on mycomputer will have read-write access to your
+ Launchpad account, including to your private data.
+ If you're using a public computer, if mycomputer is not the
+ computer you're using right now, or if something just doesn't feel
+ right about this situation, you should choose "No, thanks, I don't
+ trust 'mycomputer'", or close this window now. You can always try
+ again later.
+ Even if you decide to allow the integration, you can change your
+ mind later.
+ See all applications authorized to access Launchpad on your behalf.
+
+
+The only time the 'Desktop Integration' permission shows up in the
+list of permissions is if the client specifically requests it, and no
+other permission. (Also requesting UNAUTHORIZED is okay--it will show
+up anyway.)
+
+ >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
+ Give all programs running on "mycomputer" access to my Launchpad account.
+ No, thanks, I don't trust "mycomputer".
+
+ >>> print_access_levels_for(
+ ... 'allow_permission=DESKTOP_INTEGRATION&allow_permission=UNAUTHORIZED')
+ Give all programs running on "mycomputer" access to my Launchpad account.
+ No, thanks, I don't trust "mycomputer".
+
+A desktop may not request a level of access other than
+DESKTOP_INTEGRATION, since the whole point is to have a permission
+level that specifically applies across the entire desktop.
+
+ >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
+ Traceback (most recent call last):
+ ...
+ Unauthorized: Desktop integration token requested a permission
+ ("Change Anything") not supported for desktop-wide use.
+
+ >>> print_access_levels_for(
+ ... 'allow_permission=WRITE_PUBLIC&allow_permission=DESKTOP_INTEGRATION')
+ Traceback (most recent call last):
+ ...
+ Unauthorized: Desktop integration token requested a permission
+ ("Change Non-Private Data") not supported for desktop-wide use.
+
+You can't specify a callback URL when authorizing a desktop-wide
+token, since callback URLs should only be used when integrating
+websites into Launchpad.
+
+ >>> params['oauth_callback'] = 'http://launchpad.dev/bzr'
+ >>> print_access_levels_for('allow_permission=DESKTOP_INTEGRATION')
+ Traceback (most recent call last):
+ ...
+ Unauthorized: A desktop integration may not specify an OAuth
+ callback URL.
+
+This is true even if the desktop token isn't asking for the
+DESKTOP_INTEGRATION permission.
+
+ >>> print_access_levels_for('allow_permission=WRITE_PRIVATE')
+ Traceback (most recent call last):
+ ...
+ Unauthorized: A desktop integration may not specify an OAuth
+ callback URL.
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/canonical/launchpad/security.py'
=== modified file 'lib/canonical/launchpad/templates/oauth-authorize.pt'
--- lib/canonical/launchpad/templates/oauth-authorize.pt 2010-10-20 20:31:30 +0000
+++ lib/canonical/launchpad/templates/oauth-authorize.pt 2010-10-26 13:35:01 +0000
@@ -21,6 +21,7 @@
<tal:token-not-reviewed condition="not:token/is_reviewed">
<div metal:use-macro="context/@@launchpad_form/form">
<div metal:fill-slot="extra_top">
+<<<<<<< TREE
<tal:desktop-integration-token condition="token/consumer/is_integrated_desktop">
<h1>Confirm Computer Access</h1>
@@ -103,6 +104,71 @@
</tr>
</table>
</tal:web-integration-token>
+=======
+
+ <tal:desktop-integration-token condition="token/consumer/is_integrated_desktop">
+ <h1>Integrating
+ <tal:hostname replace="structure
+ token/consumer/integrated_desktop_name" />
+ into your Launchpad account</h1>
+ <p>The
+ <tal:desktop replace="structure
+ token/consumer/integrated_desktop_type" />
+ called
+ <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+ wants access to your Launchpad account. If you allow the
+ integration, all applications running
+ on <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+ will have read-write access to your Launchpad account,
+ including to your private data.</p>
+
+ <p>If you're using a public computer, if
+ <strong tal:content="token/consumer/integrated_desktop_name">hostname</strong>
+ is not the computer you're using right now, or if
+ something just doesn't feel right about this situation,
+ you should choose "No, thanks, I don't trust
+ '<tal:hostname replace="structure
+ token/consumer/integrated_desktop_name" />'",
+ or close this window now. You can always try
+ again later.</p>
+
+ <p>Even if you decide to allow the integration, you can
+ change your mind later.</p>
+ </tal:desktop-integration-token>
+
+ <tal:web-integration-token condition="not:token/consumer/is_integrated_desktop">
+ <h1>Integrating
+ <tal:hostname replace="structure token/consumer/key" />
+ into your Launchpad account</h1>
+
+ <p>The application identified as
+ <strong tal:content="token/consumer/key">consumer</strong>
+ wants to access
+ <tal:has-context condition="view/token_context">
+ things related to
+ <strong tal:content="view/token_context/title">Context</strong>
+ in
+ </tal:has-context>
+ Launchpad on your behalf. What level of access
+ do you want to grant?</p>
+ </tal:web-integration-token>
+
+ <table>
+ <tr tal:repeat="action view/visible_actions">
+ <td style="text-align: right">
+ <tal:action replace="structure action/render" />
+ </td>
+
+ <tal:web-integration-token
+ condition="not:token/consumer/is_integrated_desktop">
+ <td>
+ <span class="lesser"
+ tal:content="action/permission/description" />
+ </td>
+ </tal:web-integration-token>
+ </tr>
+ </table>
+>>>>>>> MERGE-SOURCE
</div>
<div metal:fill-slot="extra_bottom">
=== modified file 'lib/canonical/launchpad/tests/test_oauth_tokens.py'
--- lib/canonical/launchpad/tests/test_oauth_tokens.py 2010-10-19 18:16:29 +0000
+++ lib/canonical/launchpad/tests/test_oauth_tokens.py 2010-10-26 13:35:01 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
# Copyright 2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
@@ -411,3 +412,52 @@
self.assertRaises(
KeyError, oauth_access_token_for, self.consumer.key,
self.person, 'NO_SUCH_PERMISSION')
+=======
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from datetime import (
+ datetime,
+ timedelta
+ )
+import pytz
+
+from canonical.launchpad.webapp.interfaces import OAuthPermission
+from canonical.testing.layers import DatabaseFunctionalLayer
+
+from lp.testing import (
+ TestCaseWithFactory,
+ )
+
+
+class TestRequestTokens(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ """Set up a dummy person and OAuth consumer."""
+ super(TestRequestTokens, self).setUp()
+
+ self.person = self.factory.makePerson()
+ self.consumer = self.factory.makeOAuthConsumer()
+
+ now = datetime.now(pytz.timezone('UTC'))
+ self.a_long_time_ago = now - timedelta(hours=1000)
+
+ def testExpiredRequestTokenCantBeReviewed(self):
+ """An expired request token can't be reviewed."""
+ token = self.factory.makeOAuthRequestToken(
+ date_created=self.a_long_time_ago)
+ self.assertRaises(
+ AssertionError, token.review, self.person,
+ OAuthPermission.WRITE_PUBLIC)
+
+ def testExpiredRequestTokenCantBeExchanged(self):
+ """An expired request token can't be exchanged for an access token.
+
+ This can only happen if the token was reviewed before it expired.
+ """
+ token = self.factory.makeOAuthRequestToken(
+ date_created=self.a_long_time_ago, reviewed_by=self.person)
+ self.assertRaises(AssertionError, token.createAccessToken)
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
=== modified file 'lib/canonical/testing/layers.py'
=== modified file 'lib/lp/bugs/configure.zcml'
=== modified file 'lib/lp/bugs/model/bugtracker.py'
--- lib/lp/bugs/model/bugtracker.py 2010-10-19 01:21:14 +0000
+++ lib/lp/bugs/model/bugtracker.py 2010-10-26 13:35:01 +0000
@@ -831,3 +831,121 @@
def queryByBugTracker(self, bugtracker):
"""See IBugTrackerSet."""
return self.table.selectBy(bugtracker=bugtracker.id)
+<<<<<<< TREE
+=======
+
+
+class BugTrackerComponent(Storm):
+ """The software component in the remote bug tracker.
+
+ Most bug trackers organize bug reports by the software 'component'
+ they affect. This class provides a mapping of this upstream component
+ to the corresponding source package in the distro.
+ """
+ implements(IBugTrackerComponent)
+ __storm_table__ = 'BugTrackerComponent'
+
+ id = Int(primary=True)
+ name = Unicode(allow_none=False)
+
+ component_group_id = Int('component_group')
+ component_group = Reference(
+ component_group_id,
+ 'BugTrackerComponentGroup.id')
+
+ is_visible = Bool(allow_none=False)
+ is_custom = Bool(allow_none=False)
+
+ distribution_id = Int('distribution')
+ distribution = Reference(
+ distribution_id,
+ 'Distribution.id')
+
+ source_package_name_id = Int('source_package_name')
+ source_package_name = Reference(
+ source_package_name_id,
+ 'SourcePackageName.id')
+
+ def _get_distro_source_package(self):
+ """Retrieves the corresponding source package"""
+ if self.distribution is None or self.source_package_name is None:
+ return None
+ return self.distribution.getSourcePackage(
+ self.source_package_name)
+
+ def _set_distro_source_package(self, dsp):
+ """Links this component to its corresponding source package"""
+ if dsp is None:
+ self.distribution = None
+ self.source_package_name = None
+ else:
+ self.distribution = dsp.distribution
+ self.source_package_name = dsp.sourcepackagename
+
+ distro_source_package = property(
+ _get_distro_source_package,
+ _set_distro_source_package,
+ None,
+ """The distribution's source package for this component""")
+
+
+class BugTrackerComponentGroup(Storm):
+ """A collection of components in a remote bug tracker.
+
+ Some bug trackers organize sets of components into higher level
+ groups, such as Bugzilla's 'product'.
+ """
+ implements(IBugTrackerComponentGroup)
+ __storm_table__ = 'BugTrackerComponentGroup'
+
+ id = Int(primary=True)
+ name = Unicode(allow_none=False)
+ bug_tracker_id = Int('bug_tracker')
+ bug_tracker = Reference(bug_tracker_id, 'BugTracker.id')
+ components = ReferenceSet(
+ id,
+ BugTrackerComponent.component_group_id,
+ order_by=BugTrackerComponent.name)
+
+ def addComponent(self, component_name):
+ """Adds a component that is synced from a remote bug tracker"""
+
+ component = BugTrackerComponent()
+ component.name = component_name
+ component.component_group = self
+
+ store = IStore(BugTrackerComponent)
+ store.add(component)
+ store.flush()
+
+ return component
+
+ def getComponent(self, component_name):
+ """Retrieves a component by the given name.
+
+ None is returned if there is no component by that name in the
+ group.
+ """
+
+ if component_name is None:
+ return None
+ else:
+ return Store.of(self).find(
+ BugTrackerComponent,
+ (BugTrackerComponent.name == component_name)).one()
+
+ def addCustomComponent(self, component_name):
+ """Adds a component locally that isn't synced from a remote tracker
+ """
+
+ component = BugTrackerComponent()
+ component.name = component_name
+ component.component_group = self
+ component.is_custom = True
+
+ store = IStore(BugTrackerComponent)
+ store.add(component)
+ store.flush()
+
+ return component
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/bugs/model/tests/test_bugtask_status.py'
--- lib/lp/bugs/model/tests/test_bugtask_status.py 2010-10-25 12:57:30 +0000
+++ lib/lp/bugs/model/tests/test_bugtask_status.py 2010-10-26 13:35:01 +0000
@@ -16,6 +16,7 @@
person_logged_in,
TestCaseWithFactory,
)
+<<<<<<< TREE
class TestBugTaskStatusTransitionForUser(TestCaseWithFactory):
@@ -354,3 +355,309 @@
def makePersonAndTask(self):
self.person = getUtility(ILaunchpadCelebrities).janitor
self.task = self.factory.makeBugTask()
+=======
+
+
+class TestBugTaskStatusTransitionForUser(TestCaseWithFactory):
+ """Test bugtask status transitions for a regular logged in user."""
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestBugTaskStatusTransitionForUser, self).setUp()
+ self.user = self.factory.makePerson()
+ self.task = self.factory.makeBugTask()
+
+ def test_user_transition_all_statuses(self):
+ # A regular user should not be able to set statuses in
+ # BUG_SUPERVISOR_BUGTASK_STATUSES, but can set any
+ # other status.
+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
+ with person_logged_in(self.user):
+ self.assertRaises(
+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
+ BugTaskStatus.WONTFIX, self.user)
+ self.assertRaises(
+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
+ BugTaskStatus.EXPIRED, self.user)
+ self.assertRaises(
+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
+ BugTaskStatus.TRIAGED, self.user)
+ self.task.transitionToStatus(BugTaskStatus.NEW, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
+ self.task.transitionToStatus(
+ BugTaskStatus.INCOMPLETE, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
+ self.task.transitionToStatus(BugTaskStatus.OPINION, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.OPINION)
+ self.task.transitionToStatus(BugTaskStatus.INVALID, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.INVALID)
+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
+ self.task.transitionToStatus(
+ BugTaskStatus.INPROGRESS, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
+ self.task.transitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
+ self.task.transitionToStatus(
+ BugTaskStatus.FIXRELEASED, self.user)
+ self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
+
+ def test_user_cannot_unset_wont_fix_status(self):
+ # A regular user should not be able to transition a bug away
+ # from Won't Fix.
+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
+ with person_logged_in(self.user):
+ self.assertRaises(
+ UserCannotEditBugTaskStatus, self.task.transitionToStatus,
+ BugTaskStatus.CONFIRMED, self.user)
+
+ def test_user_canTransitionToStatus(self):
+ # Regular user cannot transition to BUG_SUPERVISOR_BUGTASK_STATUSES,
+ # but can transition to any other status.
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.WONTFIX, self.user),
+ False)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.EXPIRED, self.user),
+ False)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.TRIAGED, self.user),
+ False)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.NEW, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INCOMPLETE, self.user), True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.OPINION, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INVALID, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.CONFIRMED, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INPROGRESS, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.user),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.FIXRELEASED, self.user),
+ True)
+
+ def test_user_canTransitionToStatus_from_wontfix(self):
+ # A regular user cannot transition away from Won't Fix,
+ # so canTransitionToStatus should return False.
+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.NEW, self.user),
+ False)
+
+
+class TestBugTaskStatusTransitionForPrivilegedUserBase:
+ """Base class used to test privileged users and status transitions."""
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestBugTaskStatusTransitionForPrivilegedUserBase, self).setUp()
+ # Creation of task and target are deferred to subclasses.
+ self.task = None
+ self.person = None
+ self.makePersonAndTask()
+
+ def makePersonAndTask(self):
+ """Create a bug task and privileged person for this task.
+
+ This method is implemented by subclasses to correctly setup
+ each test.
+ """
+ raise NotImplementedError(self.makePersonAndTask)
+
+ def test_privileged_user_transition_any_status(self):
+ # Privileged users (like owner or bug supervisor) should
+ # be able to set any status.
+ with person_logged_in(self.person):
+ self.task.transitionToStatus(BugTaskStatus.WONTFIX, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.WONTFIX)
+ self.task.transitionToStatus(BugTaskStatus.EXPIRED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.EXPIRED)
+ self.task.transitionToStatus(BugTaskStatus.TRIAGED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.TRIAGED)
+ self.task.transitionToStatus(BugTaskStatus.NEW, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.NEW)
+ self.task.transitionToStatus(
+ BugTaskStatus.INCOMPLETE, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.INCOMPLETE)
+ self.task.transitionToStatus(BugTaskStatus.OPINION, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.OPINION)
+ self.task.transitionToStatus(BugTaskStatus.INVALID, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.INVALID)
+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
+ self.task.transitionToStatus(
+ BugTaskStatus.INPROGRESS, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.INPROGRESS)
+ self.task.transitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.FIXCOMMITTED)
+ self.task.transitionToStatus(
+ BugTaskStatus.FIXRELEASED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.FIXRELEASED)
+
+ def test_privileged_user_can_unset_wont_fix_status(self):
+ # Privileged users can transition away from Won't Fix.
+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
+ with person_logged_in(self.person):
+ self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
+ self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
+
+ def test_privileged_user_canTransitionToStatus(self):
+ # Privileged users (like owner or bug supervisor) should
+ # be able to set any status, so canTransitionToStatus should
+ # always return True.
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.WONTFIX, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.EXPIRED, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.TRIAGED, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.NEW, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INCOMPLETE, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.OPINION, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INVALID, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.CONFIRMED, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.INPROGRESS, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.FIXCOMMITTED, self.person),
+ True)
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.FIXRELEASED, self.person),
+ True)
+
+ def test_privileged_user_canTransitionToStatus_from_wontfix(self):
+ # A privileged user can transition away from Won't Fix, so
+ # canTransitionToStatus should return True.
+ removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
+ self.assertEqual(
+ self.task.canTransitionToStatus(
+ BugTaskStatus.NEW, self.person),
+ True)
+
+
+class TestBugTaskStatusTransitionOwnerPerson(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure owner person can transition to any status.."""
+
+ def makePersonAndTask(self):
+ self.person = self.factory.makePerson()
+ self.product = self.factory.makeProduct(owner=self.person)
+ self.task = self.factory.makeBugTask(target=self.product)
+
+
+class TestBugTaskStatusTransitionOwnerTeam(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure owner team can transition to any status.."""
+
+ def makePersonAndTask(self):
+ self.person = self.factory.makePerson()
+ self.team = self.factory.makeTeam(members=[self.person])
+ self.product = self.factory.makeProduct(owner=self.team)
+ self.task = self.factory.makeBugTask(target=self.product)
+
+
+class TestBugTaskStatusTransitionBugSupervisorPerson(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure bug supervisor person can transition to any status."""
+
+ def makePersonAndTask(self):
+ self.owner = self.factory.makePerson()
+ self.person = self.factory.makePerson()
+ self.product = self.factory.makeProduct(owner=self.owner)
+ self.task = self.factory.makeBugTask(target=self.product)
+ with person_logged_in(self.owner):
+ self.product.setBugSupervisor(self.person, self.person)
+
+
+class TestBugTaskStatusTransitionBugSupervisorTeamMember(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure bug supervisor team can transition to any status."""
+
+ def makePersonAndTask(self):
+ self.owner = self.factory.makePerson()
+ self.person = self.factory.makePerson()
+ self.team = self.factory.makeTeam(members=[self.person])
+ self.product = self.factory.makeProduct(owner=self.owner)
+ self.task = self.factory.makeBugTask(target=self.product)
+ with person_logged_in(self.owner):
+ self.product.setBugSupervisor(self.team, self.team)
+
+
+class TestBugTaskStatusTransitionBugWatchUpdater(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure bug_watch_updater can transition to any status."""
+
+ def makePersonAndTask(self):
+ self.person = getUtility(ILaunchpadCelebrities).bug_watch_updater
+ self.task = self.factory.makeBugTask()
+
+
+class TestBugTaskStatusTransitionBugImporter(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure bug_importer can transition to any status."""
+
+ def makePersonAndTask(self):
+ self.person = getUtility(ILaunchpadCelebrities).bug_importer
+ self.task = self.factory.makeBugTask()
+
+
+class TestBugTaskStatusTransitionJanitor(
+ TestBugTaskStatusTransitionForPrivilegedUserBase, TestCaseWithFactory):
+ """Tests to ensure lp janitor can transition to any status."""
+
+ def makePersonAndTask(self):
+ self.person = getUtility(ILaunchpadCelebrities).janitor
+ self.task = self.factory.makeBugTask()
+>>>>>>> MERGE-SOURCE
=== modified file 'lib/lp/bugs/tests/test_bugtracker_components.py'
--- lib/lp/bugs/tests/test_bugtracker_components.py 2010-10-15 06:01:53 +0000
+++ lib/lp/bugs/tests/test_bugtracker_components.py 2010-10-26 13:35:01 +0000
@@ -82,6 +82,7 @@
comp_c = self.factory.makeBugTrackerComponent(
u'example-c', self.comp_group, True)
+<<<<<<< TREE
self.assertIsNot(None, comp_a)
self.assertIsNot(None, comp_b)
self.assertIsNot(None, comp_c)
@@ -96,6 +97,22 @@
# Set the source package on the component
component.distro_source_package = package
self.assertIsNot(None, component.distro_source_package)
+=======
+ self.assertIsNot(None, comp_a)
+ self.assertIsNot(None, comp_b)
+ self.assertIsNot(None, comp_c)
+
+ def test_link_distro_source_package(self):
+ """Check that a link can be set to a distro source package"""
+ component = self.factory.makeBugTrackerComponent(
+ u'example', self.comp_group)
+ package = self.factory.makeDistributionSourcePackage()
+ self.assertIs(None, component.distro_source_package)
+
+ # Set the source package on the component
+ component.distro_source_package = package
+ self.assertIsNot(None, component.distro_source_package is not None)
+>>>>>>> MERGE-SOURCE
class TestBugTrackerWithComponents(TestCaseWithFactory):
=== modified file 'lib/lp/code/browser/branchlisting.py'
=== modified file 'lib/lp/code/browser/configure.zcml'
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2010-10-22 15:58:40 +0000
+++ lib/lp/code/configure.zcml 2010-10-26 13:35:01 +0000
@@ -22,6 +22,7 @@
provides="zope.publisher.interfaces.browser.IDefaultBrowserLayer"
name="code" />
+<<<<<<< TREE
<!-- Branch Merge Queues -->
<securedutility
component="lp.code.model.branchmergequeue.BranchMergeQueue"
@@ -39,6 +40,8 @@
set_attributes="owner name description configuration" />
</class>
+=======
+>>>>>>> MERGE-SOURCE
<class class="lp.code.model.codereviewvote.CodeReviewVoteReference">
<allow interface="lp.code.interfaces.codereviewvote.ICodeReviewVoteReferencePublic"/>
<require
=== modified file 'lib/lp/code/interfaces/branch.py'
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2010-10-22 13:43:50 +0000
+++ lib/lp/code/model/branch.py 2010-10-26 13:35:01 +0000
@@ -86,12 +86,20 @@
BranchTypeError,
CannotDeleteBranch,
InvalidBranchMergeProposal,
+<<<<<<< TREE
InvalidMergeQueueConfig,
)
from lp.code.event.branchmergeproposal import (
BranchMergeProposalNeedsReviewEvent,
NewBranchMergeProposalEvent,
)
+=======
+ )
+from lp.code.event.branchmergeproposal import (
+ BranchMergeProposalNeedsReviewEvent,
+ NewBranchMergeProposalEvent,
+ )
+>>>>>>> MERGE-SOURCE
from lp.code.interfaces.branch import (
BzrIdentityMixin,
DEFAULT_BRANCH_STATUS_IN_LISTING,
@@ -442,9 +450,14 @@
reviewer, registrant, review_type, _notify_listeners=False)
notify(NewBranchMergeProposalEvent(bmp))
+<<<<<<< TREE
if needs_review:
notify(BranchMergeProposalNeedsReviewEvent(bmp))
+=======
+ notify(BranchMergeProposalNeedsReviewEvent(bmp))
+
+>>>>>>> MERGE-SOURCE
return bmp
def _createMergeProposal(
=== modified file 'lib/lp/code/model/branchnamespace.py'
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py 2010-10-22 13:43:50 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2010-10-26 13:35:01 +0000
@@ -679,6 +679,7 @@
def setUp(self):
TestCaseWithFactory.setUp(self, user='test@xxxxxxxxxxxxx')
+<<<<<<< TREE
def test_notifyOnCreate_needs_review(self):
# When a merge proposal is created needing review, the
# BranchMergeProposalNeedsReviewEvent is raised as well as the usual
@@ -703,7 +704,19 @@
registrant = self.factory.makePerson()
result, events = self.assertNotifies(
[NewBranchMergeProposalEvent],
+=======
+ def test_notifyOnCreate(self):
+ """Ensure that a notification is emitted on creation"""
+ source_branch = self.factory.makeProductBranch()
+ target_branch = self.factory.makeProductBranch(
+ product=source_branch.product)
+ registrant = self.factory.makePerson()
+ result, events = self.assertNotifies(
+ [NewBranchMergeProposalEvent,
+ BranchMergeProposalNeedsReviewEvent],
+>>>>>>> MERGE-SOURCE
source_branch.addLandingTarget, registrant, target_branch)
+<<<<<<< TREE
self.assertEqual(result, events[0].object)
def test_needs_review_from_work_in_progress(self):
@@ -731,6 +744,9 @@
with person_logged_in(bmp.registrant):
self.assertNoNotification(
bmp.setStatus, BranchMergeProposalStatus.NEEDS_REVIEW)
+=======
+ self.assertEqual(result, events[0].object)
+>>>>>>> MERGE-SOURCE
def test_getNotificationRecipients(self):
"""Ensure that recipients can be added/removed with subscribe"""
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
=== modified file 'lib/lp/code/stories/branches/xx-branchmergeproposal-listings.txt'
=== modified file 'lib/lp/codehosting/scanner/tests/test_mergedetection.py'
=== modified file 'lib/lp/registry/configure.zcml'
=== modified file 'lib/lp/registry/model/person.py'
=== modified file 'lib/lp/testing/__init__.py'
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-10-26 12:53:04 +0000
+++ lib/lp/testing/factory.py 2010-10-26 13:35:01 +0000
@@ -83,6 +83,7 @@
EmailAddressStatus,
IEmailAddressSet,
)
+from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
from canonical.launchpad.interfaces.gpghandler import IGPGHandler
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
@@ -133,7 +134,10 @@
RevisionControlSystems,
)
from lp.code.errors import UnknownBranchTypeError
+<<<<<<< TREE
from lp.code.interfaces.branchmergequeue import IBranchMergeQueueSource
+=======
+>>>>>>> MERGE-SOURCE
from lp.code.interfaces.branchnamespace import get_branch_namespace
from lp.code.interfaces.branchtarget import IBranchTarget
from lp.code.interfaces.codeimport import ICodeImportSet
@@ -3178,6 +3182,7 @@
to_source=to_source, date_fulfilled=date_fulfilled,
status=status, diff_content=lfa))
+<<<<<<< TREE
# Factory methods for OAuth tokens.
def makeOAuthConsumer(self, key=None, secret=None):
if key is None:
@@ -3214,6 +3219,35 @@
consumer, reviewed_by=owner, access_level=access_level)
return request_token.createAccessToken()
+=======
+ # Factory methods for OAuth tokens.
+ def makeOAuthConsumer(self, key=None, secret=None):
+ if key is None:
+ key = self.getUniqueString("oauthconsumerkey")
+ if secret is None:
+ secret = ''
+ return getUtility(IOAuthConsumerSet).new(key, secret)
+
+ def makeOAuthRequestToken(
+ self, consumer=None, date_created=None, reviewed_by=None,
+ access_level=OAuthPermission.READ_PUBLIC):
+ """Create a (possibly reviewed) OAuth request token."""
+ if consumer is None:
+ consumer = self.makeOAuthConsumer()
+ token = consumer.newRequestToken()
+
+ if reviewed_by is not None:
+ # Review the token before modifying the date_created,
+ # since the date_created can be used to simulate an
+ # expired token.
+ token.review(reviewed_by, access_level)
+
+ if date_created is not None:
+ unwrapped_token = removeSecurityProxy(token)
+ unwrapped_token.date_created = date_created
+ return token
+
+>>>>>>> MERGE-SOURCE
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by