← Back to team overview

launchpad-reviewers team mailing list archive

[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 &quot;%s&quot; 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 &quot;%s&quot;."
+            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