← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel

 

Robert Collins has proposed merging lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers): release-critical


Revision 11516 can be deployed: orphaned
Revision 11517 can be deployed: orphaned
Revision 11518 can be deployed: qa-ok
  [r=michael.nelson][ui=none][bug=627741] Remove unnecessary assert
        breaking oops-prune
Revision 11519 can be deployed: orphaned
Revision 11520 can be deployed: qa-ok
  [r=leonardr][ui=none][bug=623408][incr] Replaces use of
        official_rosetta with translations_usage where possible,
        so the richer data provided by the enum can be used to drive decisions.
Revision 11521 can be deployed: qa-ok
  [r=leonardr][ui=none][bug=237722] Catch CyclicalTeamMembershipError
        and display a pleasing warning message rather than filing an
        OOPS when +editproposedmembers would create a cycle.
Revision 11522 can be deployed: qa-ok
  [r=mwhudson][ui=none][bug=583553] Remove unnecessary queries for LibraryFileContent on LibraryFileAlias.deleted.
Revision 11523 can be deployed: qa-ok
  [r=thumper][ui=none][bug=627940, 630612,\n\t632022] Gather time spent in google searches into the request timeline.
Revision 11524 can be deployed: qa-ok
  [r=thumper][ui=rockstar][bug=336685] Change to heading text on lp
        code home page to indicate that the project tag cloud only
        contains a selection of active projects
Revision 11525 can be deployed: qa-ok
  [r=mwhudson][ui=none][bug=634451] Fix the db permissions for updating
        bugheat from the scanner when linking branches.
Revision 11526 can be deployed: qa-ok
  [r=lifeless][ui=none][bug=632880] Generate URLs straight from
        LibraryFileAlias.
Revision 11527 can be deployed: qa-ok
  [r=sinzui][ui=none][bug=633926] Fix a regression where users are not
        allowed to change their own name. Rewrote the test to *not* use
        an administrator to demonstrate the action,
        which masked the regression.
Revision 11528 can be deployed: qa-ok
  [r=rockstar][ui=none][bug=623408][incr] Replaces use of
        official_answers and official_blueprints with their usage enum
        equivalents.
Revision 11529 can be deployed: qa-ok
  [r=adeuring][ui=none][bug=634045] Parse Launchpad-Bugs-Fixed
        regardless of casing.
Revision 11530 can be deployed: qa-ok
  [r=lifeless][ui=none][bug 591544] Do not query TeamParticipation in
        inTeam when the team is in fact a person. Use ids instead of
        instance objects when checking is_team.
Revision 11531 can be deployed: qa-ok
  [r=adeuring][ui=none][bug=589349] Display the number of the duplicate
        bug to which a subscriber is subscribed in the subscriptions
        from duplicates portlet.
Revision 11532 can be deployed: qa-ok
Revision 11533 can be deployed: qa-ok
Revision 11534 can be deployed: qa-ok
Revision 11535 can be deployed: qa-ok
Revision 11536 can be deployed: qa-ok
  [r=mwhudson][ui=none][bug=634342] Permit pageid based feature scopes.
Revision 11537 can be deployed: qa-ok
  [r=bigjools][ui=rs][bug=634859] Change the UI name of builder.active
        from 'Active' to 'Present Publicly' to reduce confusion about
        its meaning. (Landed for LaMont)
Revision 11538 can be deployed: qa-ok
Revision 11539 can be deployed: qa-ok
  [r=henninge][ui=none][bug=581626] show links to the bug targets in
        the "recently reported" and "recently fixed" lists of LPs main
        bug page.
Revision 11540 can be deployed: qa-ok
  [r=stevenk][ui=none][no-qa] Permit disabling memcache via feature flags.
Revision 11541 can be deployed: qa-ok
Revision 11542 can be deployed: qa-ok
  [r=abentley][ui=none][bug=608349] Move DistroSeries:+templates out of
        TAL.
Revision 11543 can be deployed: qa-ok
  [r=mwhudson][ui=none][no-qa] Manually close actions when the StatementTracer will not run it closing code path.
Revision 11544 can be deployed: qa-ok
  [r=thumper][ui=none][bug=615237, 627412,
        637654] Help timeouts in mailing list pages by batching.
Revision 11545 can be deployed: qa-ok
  [r=lifeless][ui=none][bug=637868] Fix 2.6'ism in +templates pages.
Revision 11546 can be deployed: orphaned
  [testfix][rs=jml] Fix shallow test failure in test_adapter,
        based on instructions from lifeless.

-- 
https://code.launchpad.net/~lifeless/launchpad/cp/+merge/35764
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/cp into lp:~launchpad-pqm/launchpad/production-devel.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2010-09-03 16:43:11 +0000
+++ database/schema/security.cfg	2010-09-17 01:08:50 +0000
@@ -593,6 +593,7 @@
 public.diff                             = SELECT, INSERT, DELETE
 public.distroseries                     = SELECT
 public.distribution                     = SELECT
+public.distributionsourcepackage        = SELECT, UPDATE
 public.emailaddress                     = SELECT
 public.job                              = SELECT, INSERT, UPDATE, DELETE
 # Karma

=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf	2010-08-30 00:51:55 +0000
+++ lib/canonical/config/schema-lazr.conf	2010-09-17 01:08:50 +0000
@@ -41,6 +41,7 @@
 
 
 [branchscanner]
+branch_revision_delete_count: 100
 # The database user which will be used by this process.
 # datatype: string
 dbuser: branchscanner

=== modified file 'lib/canonical/launchpad/database/librarian.py'
--- lib/canonical/launchpad/database/librarian.py	2010-09-06 21:02:44 +0000
+++ lib/canonical/launchpad/database/librarian.py	2010-09-17 01:08:50 +0000
@@ -125,7 +125,7 @@
     @property
     def http_url(self):
         """See ILibraryFileAlias.http_url"""
-        return self.client.getURLForAlias(self.id)
+        return self.client.getURLForAliasObject(self)
 
     @property
     def https_url(self):
@@ -236,7 +236,7 @@
 
     @property
     def deleted(self):
-        return self.content is None
+        return self.contentID is None
 
     def __storm_invalidated__(self):
         """Make sure that the file is closed across transaction boundary."""

=== modified file 'lib/canonical/launchpad/doc/textsearching.txt'
--- lib/canonical/launchpad/doc/textsearching.txt	2010-08-02 02:33:53 +0000
+++ lib/canonical/launchpad/doc/textsearching.txt	2010-09-17 01:08:50 +0000
@@ -128,7 +128,7 @@
     ...     try:
     ...         cur.execute("SELECT _ftq(%s), ftq(%s)", (query, query))
     ...         uncompiled, compiled = cur.fetchone()
-    ...     except:
+    ...     except Exception:
     ...         SQLBase._connection._connection.rollback()
     ...         raise
     ...     if uncompiled is not None:

=== modified file 'lib/canonical/launchpad/doc/timeout.txt'
--- lib/canonical/launchpad/doc/timeout.txt	2010-03-09 18:41:20 +0000
+++ lib/canonical/launchpad/doc/timeout.txt	2010-09-17 01:08:50 +0000
@@ -10,7 +10,7 @@
 time remaining before the request should time out.
 
     >>> from canonical.lazr.timeout import get_default_timeout_function
-    >>> from canonical.launchpad.webapp.servers import (
+    >>> from canonical.launchpad.webapp.adapter import (
     ...     set_launchpad_default_timeout)
     >>> old_func = get_default_timeout_function()
 
@@ -21,7 +21,7 @@
     >>> set_launchpad_default_timeout(ProcessStarting())
 
     >>> get_default_timeout_function()
-    <function launchpad_default_timeout...>
+    <function get_request_remaining_seconds...>
 
 The timeout to use is the number of seconds remaining before
 db_statement_timeout is expired.
@@ -30,7 +30,7 @@
     >>> from canonical.config import config
     >>> config.push('timeout', dedent('''\
     ... [database]
-    ... db_statement_timeout = 10'''))
+    ... db_statement_timeout = 10000'''))
 
     >>> timeout_func = get_default_timeout_function()
 

=== modified file 'lib/canonical/launchpad/scripts/ftests/raiseexception.py'
--- lib/canonical/launchpad/scripts/ftests/raiseexception.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/scripts/ftests/raiseexception.py	2010-09-17 01:08:50 +0000
@@ -29,7 +29,7 @@
     print >> sys.stderr, 'Script Output'
     try:
         raise RuntimeError('Aargh')
-    except:
+    except RuntimeError:
         log.exception('Oops')
         root_log.exception('Root oops')
 

=== modified file 'lib/canonical/launchpad/scripts/oops.py'
--- lib/canonical/launchpad/scripts/oops.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/scripts/oops.py	2010-09-17 01:08:50 +0000
@@ -68,10 +68,6 @@
                 if match.group('oops') is not None:
                     code_string = match.group('oopscode')
                     referenced_codes.add(code_string.upper())
-                    found = True
-            assert found, \
-                'PostgreSQL regexp matched content that Python regexp ' \
-                'did not (%r)' % (content,)
 
     return referenced_codes
 

=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py	2010-09-03 16:43:11 +0000
+++ lib/canonical/launchpad/security.py	2010-09-17 01:08:50 +0000
@@ -6,7 +6,10 @@
 """Security policies for using content objects."""
 
 __metaclass__ = type
-__all__ = ['AuthorizationBase']
+__all__ = [
+    'AnonymousAuthorization',
+    'AuthorizationBase',
+    ]
 
 from zope.component import (
     getAdapter,
@@ -21,7 +24,6 @@
 from canonical.launchpad.interfaces.account import IAccount
 from canonical.launchpad.interfaces.emailaddress import IEmailAddress
 from canonical.launchpad.interfaces.launchpad import (
-    IHasBug,
     IHasDrivers,
     ILaunchpadCelebrities,
     IPersonRoles,
@@ -29,7 +31,6 @@
 from canonical.launchpad.interfaces.librarian import (
     ILibraryFileAliasWithParent,
     )
-from canonical.launchpad.interfaces.message import IMessage
 from canonical.launchpad.interfaces.oauth import (
     IOAuthAccessToken,
     IOAuthRequestToken,
@@ -50,13 +51,6 @@
     )
 from lp.blueprints.interfaces.sprint import ISprint
 from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
-from lp.bugs.interfaces.bug import IBug
-from lp.bugs.interfaces.bugattachment import IBugAttachment
-from lp.bugs.interfaces.bugbranch import IBugBranch
-from lp.bugs.interfaces.bugnomination import IBugNomination
-from lp.bugs.interfaces.bugsubscription import IBugSubscription
-from lp.bugs.interfaces.bugtracker import IBugTracker
-from lp.bugs.interfaces.bugwatch import IBugWatch
 from lp.buildmaster.interfaces.builder import (
     IBuilder,
     IBuilderSet,
@@ -399,14 +393,6 @@
     usedfor = IOAuthRequestToken
 
 
-class EditBugNominationStatus(AuthorizationBase):
-    permission = 'launchpad.Driver'
-    usedfor = IBugNomination
-
-    def checkAuthenticated(self, user):
-        return self.obj.canApprove(user.person)
-
-
 class EditByOwnersOrAdmins(AuthorizationBase):
     permission = 'launchpad.Edit'
     usedfor = IHasOwner
@@ -991,115 +977,6 @@
         return EditByOwnersOrAdmins.checkAuthenticated(self, user)
 
 
-class EditBugTask(AuthorizationBase):
-    """Permission checker for editing objects linked to a bug.
-
-    Allow any logged-in user to edit objects linked to public
-    bugs. Allow only explicit subscribers to edit objects linked to
-    private bugs.
-    """
-    permission = 'launchpad.Edit'
-    usedfor = IHasBug
-
-    def checkAuthenticated(self, user):
-        # Delegated entirely to the bug.
-        return self.obj.bug.userCanView(user)
-
-
-class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):
-    permission = 'launchpad.View'
-    usedfor = IHasBug
-
-    def checkAuthenticated(self, user):
-        return self.obj.bug.userCanView(user.person)
-
-    def checkUnauthenticated(self):
-        """Allow anonymous users to see non-private bugs only."""
-        return not self.obj.bug.private
-
-
-class EditPublicByLoggedInUserAndPrivateByExplicitSubscribers(
-    AuthorizationBase):
-    permission = 'launchpad.Edit'
-    usedfor = IBug
-
-    def checkAuthenticated(self, user):
-        """Allow any logged in user to edit a public bug, and only
-        explicit subscribers to edit private bugs. Any bug that can be seen can
-        be edited.
-        """
-        return self.obj.userCanView(user)
-
-    def checkUnauthenticated(self):
-        """Never allow unauthenticated users to edit a bug."""
-        return False
-
-
-class PublicToAllOrPrivateToExplicitSubscribersForBug(AuthorizationBase):
-    permission = 'launchpad.View'
-    usedfor = IBug
-
-    def checkAuthenticated(self, user):
-        """Allow any user to see non-private bugs, but only explicit
-        subscribers to see private bugs.
-        """
-        return self.obj.userCanView(user.person)
-
-    def checkUnauthenticated(self):
-        """Allow anonymous users to see non-private bugs only."""
-        return not self.obj.private
-
-
-class EditBugBranch(EditPublicByLoggedInUserAndPrivateByExplicitSubscribers):
-    permission = 'launchpad.Edit'
-    usedfor = IBugBranch
-
-    def __init__(self, bug_branch):
-        # The same permissions as for the BugBranch's bug should apply
-        # to the BugBranch itself.
-        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
-            self, bug_branch.bug)
-
-
-class ViewBugAttachment(PublicToAllOrPrivateToExplicitSubscribersForBug):
-    """Security adapter for viewing a bug attachment.
-
-    If the user is authorized to view the bug, he's allowed to view the
-    attachment.
-    """
-    permission = 'launchpad.View'
-    usedfor = IBugAttachment
-
-    def __init__(self, bugattachment):
-        PublicToAllOrPrivateToExplicitSubscribersForBug.__init__(
-            self, bugattachment.bug)
-
-
-class EditBugAttachment(
-    EditPublicByLoggedInUserAndPrivateByExplicitSubscribers):
-    """Security adapter for editing a bug attachment.
-
-    If the user is authorized to view the bug, he's allowed to edit the
-    attachment.
-    """
-    permission = 'launchpad.Edit'
-    usedfor = IBugAttachment
-
-    def __init__(self, bugattachment):
-        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
-            self, bugattachment.bug)
-
-
-class ViewBugSubscription(AnonymousAuthorization):
-
-    usedfor = IBugSubscription
-
-
-class ViewBugMessage(AnonymousAuthorization):
-
-    usedfor = IMessage
-
-
 class ViewAnnouncement(AuthorizationBase):
     permission = 'launchpad.View'
     usedfor = IAnnouncement
@@ -1418,20 +1295,6 @@
              user.inTeam(translation_group.owner)))
 
 
-class ViewBugTracker(AnonymousAuthorization):
-    """Anyone can view a bug tracker."""
-    usedfor = IBugTracker
-
-
-class EditBugTracker(AuthorizationBase):
-    permission = 'launchpad.Edit'
-    usedfor = IBugTracker
-
-    def checkAuthenticated(self, user):
-        """Any logged-in user can edit a bug tracker."""
-        return True
-
-
 class EditProductRelease(EditByOwnersOrAdmins):
     permission = 'launchpad.Edit'
     usedfor = IProductRelease
@@ -2580,24 +2443,3 @@
         if parent is None:
             return False
         return check_permission(self.permission, parent)
-
-
-class AdminBugTracker(AuthorizationBase):
-    permission = 'launchpad.Admin'
-    usedfor = IBugTracker
-
-    def checkAuthenticated(self, user):
-        return (
-            user.in_janitor or
-            user.in_admin or
-            user.in_launchpad_developers)
-
-
-class AdminBugWatch(AuthorizationBase):
-    permission = 'launchpad.Admin'
-    usedfor = IBugWatch
-
-    def checkAuthenticated(self, user):
-        return (
-            user.in_admin or
-            user.in_launchpad_developers)

=== modified file 'lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt'
--- lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt	2010-02-25 00:47:17 +0000
+++ lib/canonical/launchpad/templates/batchnavigator-navigation-links.pt	2010-09-17 01:08:50 +0000
@@ -27,9 +27,9 @@
           class="batch-navigation-links">
         <a
           tal:condition="first_page_url"
-          tal:attributes="href first_page_url"
+          tal:attributes="href first_page_url;
+                          id string:${view/css_class}-batchnav-first"
           class="first"
-          id="batchnav_first"
           rel="first"
         >First</a>
         <span tal:condition="not:first_page_url" class="first inactive"
@@ -37,9 +37,9 @@
         &#149;
         <a
           tal:condition="prev_page_url"
-          tal:attributes="href prev_page_url"
+          tal:attributes="href prev_page_url;
+                          id string:${view/css_class}-batchnav-previous"
           class="previous"
-          id="batchnav_previous"
           rel="previous"
         >Previous</a>
         <span tal:condition="not:prev_page_url" class="previous inactive"
@@ -47,8 +47,8 @@
         &#149;
         <a
           tal:condition="next_page_url"
-          tal:attributes="href next_page_url"
-          id="batchnav_next"
+          tal:attributes="href next_page_url;
+                          id string:${view/css_class}-batchnav-next"
           class="next"
           rel="next"
         ><strong>Next</strong></a>
@@ -58,9 +58,9 @@
           &#149;
           <a
             tal:condition="last_page_url"
-            tal:attributes="href last_page_url"
+            tal:attributes="href last_page_url;
+                            id string:${view/css_class}-batchnav-last"
             class="last"
-            id="batchnav_last"
             rel="last"
           >Last</a>
           <span tal:condition="not:last_page_url" class="last inactive"

=== modified file 'lib/canonical/launchpad/utilities/searchservice.py'
--- lib/canonical/launchpad/utilities/searchservice.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/utilities/searchservice.py	2010-09-17 01:08:50 +0000
@@ -20,6 +20,7 @@
 import urllib
 from urlparse import urlunparse
 
+from lazr.restful.utils import get_current_browser_request
 from lazr.uri import URI
 from zope.interface import implements
 
@@ -32,6 +33,7 @@
     ISearchService,
     )
 from canonical.launchpad.webapp import urlparse
+from lp.services.timeline.requesttimeline import get_request_timeline
 
 
 class PageMatch:
@@ -193,8 +195,13 @@
         """
         search_url = self.create_search_url(terms, start=start)
         from canonical.lazr.timeout import urlfetch
-        gsp_xml = urlfetch(search_url)
-
+        request = get_current_browser_request()
+        timeline = get_request_timeline(request)
+        action = timeline.start("google-search-api", search_url)
+        try:
+            gsp_xml = urlfetch(search_url)
+        finally:
+            action.finish()
         page_matches = self._parse_google_search_protocol(gsp_xml)
         return page_matches
 
@@ -251,7 +258,6 @@
         """
         return self._getElementsByAttributeValue(doc, path, name, value)[0]
 
-
     def _parse_google_search_protocol(self, gsp_xml):
         """Return a `PageMatches` object.
 

=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py	2010-09-14 05:16:26 +0000
+++ lib/canonical/launchpad/webapp/adapter.py	2010-09-17 01:08:50 +0000
@@ -66,6 +66,7 @@
     )
 from canonical.launchpad.webapp.opstats import OpStats
 from canonical.lazr.utils import get_current_browser_request, safe_hasattr
+from canonical.lazr.timeout import set_default_timeout_function
 from lp.services.timeline.timeline import Timeline
 from lp.services.timeline.requesttimeline import (
     get_request_timeline,
@@ -82,6 +83,7 @@
     'get_request_duration',
     'get_store_name',
     'hard_timeout_expired',
+    'launchpad_default_timeout',
     'soft_timeout_expired',
     'StoreSelector',
     ]
@@ -104,11 +106,18 @@
         return ('Statement: %r\nParameters:%r\nOriginal error: %r'
                 % (self.statement, self.params, self.original_error))
 
+
+class RequestExpired(RuntimeError):
+    """Request has timed out."""
+    implements(IRequestExpired)
+
+
 def _get_dirty_commit_flags():
     """Return the current dirty commit status"""
     from canonical.ftests.pgsql import ConnectionWrapper
     return (ConnectionWrapper.committed, ConnectionWrapper.dirty)
 
+
 def _reset_dirty_commit_flags(previous_committed, previous_dirty):
     """Set the dirty commit status to False unless previous is True"""
     from canonical.ftests.pgsql import ConnectionWrapper
@@ -248,32 +257,48 @@
     return now - starttime
 
 
-def _check_expired(timeout):
-    """Checks whether the current request has passed the given timeout."""
-    if timeout is None or not getattr(_local, 'enable_timeout', True):
-        return False # no timeout configured or timeout disabled.
-
-    starttime = getattr(_local, 'request_start_time', None)
-    if starttime is None:
-        return False # no current request
-
-    requesttime = (time() - starttime) * 1000
-    return requesttime > timeout
-
-
-def hard_timeout_expired():
-    """Returns True if the hard request timeout been reached."""
-    return _check_expired(config.database.db_statement_timeout)
+def get_request_remaining_seconds(no_exception=False, now=None, timeout=None):
+    """Return how many seconds are remaining in the current request budget.
+
+    If timouts are disabled, None is returned. 
+
+    :param no_exception: If True, do not raise an error if the request
+        is out of time. Instead return a float e.g. -2.0 for 2 seconds over
+        budget.
+    :param now: Override the result of time.time()
+    :param timeout: A custom timeout in ms.
+    :return: None or a float representing the remaining time budget.
+    """
+    if not getattr(_local, 'enable_timeout', True):
+        return None
+    if timeout is None:
+        timeout = config.database.db_statement_timeout
+    if not timeout:
+        return None
+    duration = get_request_duration(now)
+    if duration == -1:
+        return None
+    remaining = timeout / 1000.0 - duration
+    if remaining <= 0:
+        if no_exception:
+            return remaining
+        raise RequestExpired('request expired.')
+    return remaining
+
+
+def set_launchpad_default_timeout(event):
+    """Set the LAZR default timeout function on IProcessStartingEvent."""
+    set_default_timeout_function(get_request_remaining_seconds)
 
 
 def soft_timeout_expired():
     """Returns True if the soft request timeout been reached."""
-    return _check_expired(config.database.soft_request_timeout)
-
-
-class RequestExpired(RuntimeError):
-    """Request has timed out."""
-    implements(IRequestExpired)
+    try:
+        get_request_remaining_seconds(
+            timeout=config.database.soft_request_timeout)
+        return False
+    except RequestExpired:
+        return True
 
 
 # ---- Prevent database access in the main thread of the app server
@@ -468,7 +493,7 @@
 
     @property
     def granularity(self):
-        return dbconfig.db_statement_timeout_precision / 1000.0
+        return config.database.db_statement_timeout_precision / 1000.0
 
     def connection_raw_execute(self, connection, raw_cursor,
                                statement, params):
@@ -478,12 +503,15 @@
         if not isinstance(connection._database, LaunchpadDatabase):
             return
         # If we are outside of a request, don't do timeout adjustment.
-        if self.get_remaining_time() is None:
-            return
         try:
+            if self.get_remaining_time() is None:
+                return
             super(LaunchpadTimeoutTracer, self).connection_raw_execute(
                 connection, raw_cursor, statement, params)
-        except TimeoutError:
+        except (RequestExpired, TimeoutError):
+            # XXX: This code does not belong here - see bug=636804.
+            # Robert Collins 20100913.
+            OpStats.stats['timeouts'] += 1
             # XXX bug=636801 Robert Colins 20100914 This is duplicated from the
             # statement tracer, because the tracers are not arranged in a stack
             # rather a queue: the done-code in the statement tracer never runs.
@@ -494,7 +522,6 @@
                 action.finish()
             info = sys.exc_info()
             transaction.doom()
-            OpStats.stats['timeouts'] += 1
             try:
                 raise info[0], info[1], info[2]
             finally:
@@ -513,15 +540,7 @@
 
     def get_remaining_time(self):
         """See `TimeoutTracer`"""
-        if (not dbconfig.db_statement_timeout or
-            not getattr(_local, 'enable_timeout', True)):
-            return None
-        start_time = getattr(_local, 'request_start_time', None)
-        if start_time is None:
-            return None
-        now = time()
-        ellapsed = now - start_time
-        return  dbconfig.db_statement_timeout / 1000.0 - ellapsed
+        return get_request_remaining_seconds()
 
 
 class LaunchpadStatementTracer:

=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
--- lib/canonical/launchpad/webapp/configure.zcml	2010-08-13 14:42:58 +0000
+++ lib/canonical/launchpad/webapp/configure.zcml	2010-09-17 01:08:50 +0000
@@ -785,7 +785,7 @@
     <!-- Set the default timeout function. -->
     <subscriber
         for="zope.app.appsetup.IProcessStartingEvent"
-        handler="canonical.launchpad.webapp.servers.set_launchpad_default_timeout"
+        handler="canonical.launchpad.webapp.adapter.set_launchpad_default_timeout"
         />
 
     <subscriber

=== modified file 'lib/canonical/launchpad/webapp/errorlog.py'
--- lib/canonical/launchpad/webapp/errorlog.py	2010-09-04 20:18:39 +0000
+++ lib/canonical/launchpad/webapp/errorlog.py	2010-09-17 01:08:50 +0000
@@ -507,7 +507,7 @@
             # We disable the pylint warning for the blank except.
             try:
                 raise info[0], info[1], traceback
-            except:
+            except info[0]:
                 logging.getLogger('SiteError').exception(
                     '%s (%s)' % (url, oopsid))
 

=== modified file 'lib/canonical/launchpad/webapp/ftests/test_adapter.txt'
--- lib/canonical/launchpad/webapp/ftests/test_adapter.txt	2010-09-14 11:00:02 +0000
+++ lib/canonical/launchpad/webapp/ftests/test_adapter.txt	2010-09-17 01:08:50 +0000
@@ -296,7 +296,7 @@
     >>> store.execute('SELECT 2', noresult=True)
     Traceback (most recent call last):
     ...
-    TimeoutError: ...SELECT 2...
+    RequestExpired: request expired.
 
 The statement about to be executed is not recorded in the statement log.
 The request time limit was exceeded before the statement was issued to
@@ -359,7 +359,7 @@
     >>> store.execute('SELECT 1', noresult=True)
     Traceback (most recent call last):
     ...
-    TimeoutError: ...SELECT 1...
+    RequestExpired: request expired.
     >>> statement_issued.set()
     >>> thread.join()
     >>> clear_request_started()

=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
--- lib/canonical/launchpad/webapp/interfaces.py	2010-08-24 16:44:42 +0000
+++ lib/canonical/launchpad/webapp/interfaces.py	2010-09-17 01:08:50 +0000
@@ -876,7 +876,7 @@
 
 try:
     from zope.publisher.interfaces import StartRequestEvent
-except:
+except ImportError:
     class IStartRequestEvent(Interface):
         """An event that gets sent before the start of a request."""
 

=== modified file 'lib/canonical/launchpad/webapp/publication.py'
--- lib/canonical/launchpad/webapp/publication.py	2010-08-20 20:31:18 +0000
+++ lib/canonical/launchpad/webapp/publication.py	2010-09-17 01:08:50 +0000
@@ -246,7 +246,7 @@
         threadrequestfile = open('thread-%s.request' % threadid, 'w')
         try:
             request_txt = unicode(request).encode('UTF-8')
-        except:
+        except Exception:
             request_txt = 'Exception converting request to string\n\n'
             try:
                 request_txt += traceback.format_exc()
@@ -446,7 +446,7 @@
 
         # The view may be security proxied
         view = removeSecurityProxy(ob)
-        # It's possible that the view is a bounded method.
+        # It's possible that the view is a bound method.
         view = getattr(view, 'im_self', view)
         context = removeSecurityProxy(getattr(view, 'context', None))
         pageid = self.constructPageID(view, context)

=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py	2010-09-04 09:57:29 +0000
+++ lib/canonical/launchpad/webapp/servers.py	2010-09-17 01:08:50 +0000
@@ -76,10 +76,6 @@
     TimestampOrderingError,
     )
 import canonical.launchpad.layers
-from canonical.launchpad.webapp.adapter import (
-    get_request_duration,
-    RequestExpired,
-    )
 from canonical.launchpad.webapp.authentication import (
     check_oauth_signature,
     get_oauth_authorization,
@@ -113,7 +109,6 @@
     )
 from canonical.launchpad.webapp.vhosts import allvhosts
 from canonical.lazr.interfaces.feed import IFeed
-from canonical.lazr.timeout import set_default_timeout_function
 from lp.app.errors import UnexpectedFormData
 from lp.services.features.flags import NullFeatureController
 from lp.services.propertycache import cachedproperty
@@ -1489,22 +1484,6 @@
         return "Protocol error: %s" % self.status
 
 
-def launchpad_default_timeout():
-    """Return the time before the request should be expired."""
-    timeout = config.database.db_statement_timeout
-    if timeout is None:
-        return None
-    left = timeout - get_request_duration()
-    if left < 0:
-        raise RequestExpired('request expired.')
-    return left
-
-
-def set_launchpad_default_timeout(event):
-    """Set the LAZR default timeout function."""
-    set_default_timeout_function(launchpad_default_timeout)
-
-
 def register_launchpad_request_publication_factories():
     """Register our factories with the Zope3 publisher.
 

=== modified file 'lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt'
--- lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt	2008-06-24 07:57:39 +0000
+++ lib/canonical/launchpad/webapp/tests/test_request_expire_render.txt	2010-09-17 01:08:50 +0000
@@ -9,9 +9,8 @@
     ...     implements(IRequestExpired)
 
 After a timeout has happened, we can no longer do any more db queries,
-since _check_expired() returns True. However, if the user is logged in,
-a query will be issued to render the "Logged in as No Privileges
-Person".
+since get_request_remaining_seconds will raise. However, if the user is logged
+in, a query will be issued to render the "Logged in as No Privileges Person".
 
     >>> from textwrap import dedent
     >>> from canonical.config import config
@@ -23,12 +22,14 @@
 
     >>> from time import time
     >>> from canonical.launchpad.webapp.adapter import (
-    ...     _check_expired, set_request_started)
+    ...     get_request_remaining_seconds, set_request_started)
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> too_early = time() - (config.database.db_statement_timeout / 1000 + 1)
     >>> set_request_started(too_early)
-    >>> _check_expired(config.database.db_statement_timeout)
-    True
+    >>> get_request_remaining_seconds()
+    Traceback (most recent call last):
+    ...
+    RequestExpired: request expired.
 
 Before the OOPS page is rendered, the timeout is reset, so that we can
 issue the DB query and render the page, showing the OOPS id to the user.
@@ -44,8 +45,8 @@
     >>> timeout_exception = TimeoutException()
     >>> exception_view = getMultiAdapter(
     ...     (timeout_exception, request), name="index.html")
-    >>> _check_expired(config.database.db_statement_timeout)
-    False
+    >>> get_request_remaining_seconds() > 0
+    True
     >>> print exception_view()
     <...OOPS_ID_MARKER...
 

=== modified file 'lib/canonical/librarian/client.py'
--- lib/canonical/librarian/client.py	2010-09-05 21:53:21 +0000
+++ lib/canonical/librarian/client.py	2010-09-17 01:08:50 +0000
@@ -53,6 +53,14 @@
     return '/%d/%s' % (int(aliasID), url_path_quote(filename))
 
 
+def compose_url(base_url, alias_path):
+    """Compose a URL for a library file alias."""
+    if alias_path is None:
+        return None
+    else:
+        return urljoin(base_url, alias_path)
+
+
 class FileUploadClient:
     """Simple blocking client for uploading to the librarian."""
 
@@ -83,7 +91,7 @@
     def _checkError(self):
         if select([self.state.s], [], [], 0)[0]:
             response = self.state.f.readline().strip()
-            raise UploadFailed, 'Server said: ' + response
+            raise UploadFailed('Server said: ' + response)
 
     def _sendLine(self, line):
         self.state.f.write(line + '\r\n')
@@ -172,7 +180,7 @@
             # Read response
             response = self.state.f.readline().strip()
             if response != '200':
-                raise UploadFailed, 'Server said: ' + response
+                raise UploadFailed('Server said: ' + response)
 
             # Add rows to DB
             content = LibraryFileContent(
@@ -187,7 +195,7 @@
             Store.of(content).flush()
 
             assert isinstance(aliasID, (int, long)), \
-                    "aliasID %r not an integer" % (aliasID,)
+                    "aliasID %r not an integer" % (aliasID, )
             return aliasID
         finally:
             self._close()
@@ -240,7 +248,7 @@
             # Read response
             response = self.state.f.readline().strip()
             if not response.startswith('200'):
-                raise UploadFailed, 'Server said: ' + response
+                raise UploadFailed('Server said: ' + response)
 
             status, ids = response.split()
             contentID, aliasID = ids.split('/', 1)
@@ -295,6 +303,43 @@
     #         raise DownloadFailed, 'Incomplete response'
     #     return paths
 
+    def _getAlias(self, aliasID, secure=False):
+        """Retrieve the `LibraryFileAlias` with the given id.
+
+        :param aliasID: A unique ID for the alias.
+        :param secure: Controls the behaviour when looking up restricted
+            files.  If False restricted files are only permitted when
+            self.restricted is True.  See `getURLForAlias`.
+        :returns: A `LibraryFileAlias`.
+        :raises: `DownloadFailed` if the alias is invalid or
+            inaccessible.
+        """
+        from canonical.launchpad.database import LibraryFileAlias
+        from sqlobject import SQLObjectNotFound
+        try:
+            lfa = LibraryFileAlias.get(aliasID)
+        except SQLObjectNotFound:
+            lfa = None
+
+        if lfa is None:
+            raise DownloadFailed('Alias %d not found' % aliasID)
+        self._checkAliasAccess(lfa, secure=secure)
+
+        return lfa
+
+    def _checkAliasAccess(self, alias, secure=False):
+        """Verify that `alias` can be accessed.
+
+        :param alias: A `LibraryFileAlias`.
+        :param secure: Controls the behaviour when looking up restricted
+            files.  If False restricted files are only permitted when
+            self.restricted is True.  See `getURLForAlias`.
+        :raises: `DownloadFailed` if access is not allowed.
+        """
+        if not secure and alias.restricted != self.restricted:
+            raise DownloadFailed(
+                'Alias %d cannot be downloaded from this client.' % alias.id)
+
     def _getPathForAlias(self, aliasID, secure=False):
         """Returns the path inside the librarian to talk about the given
         alias.
@@ -302,77 +347,95 @@
         :param aliasID: A unique ID for the alias
         :param secure: Controls the behaviour when looking up restricted
             files.  If False restricted files are only permitted when
-            self.restricted is True. See getURLForAlias.
+            self.restricted is True.  See `getURLForAlias`.
         :returns: String path, url-escaped.  Unicode is UTF-8 encoded before
             url-escaping, as described in section 2.2.5 of RFC 2718.
             None if the file has been deleted.
+
         :raises: DownloadFailed if the alias is invalid
         """
-        from canonical.launchpad.database import LibraryFileAlias
-        from sqlobject import SQLObjectNotFound
-        aliasID = int(aliasID)
-        try:
-            # Use SQLObjects to maximize caching benefits
-            lfa = LibraryFileAlias.get(aliasID)
-        except SQLObjectNotFound:
-            raise DownloadFailed('Alias %d not found' % aliasID)
-        if not secure and self.restricted != lfa.restricted:
-            raise DownloadFailed(
-                'Alias %d cannot be downloaded from this client.' % aliasID)
-        if lfa.deleted:
+        return self._getPathForAliasObject(
+            self._getAlias(int(aliasID), secure=secure))
+
+    def _getPathForAliasObject(self, alias):
+        """Returns the Librarian path for a `LibraryFileAlias`."""
+        if alias.deleted:
             return None
         return get_libraryfilealias_download_path(
-            aliasID, lfa.filename.encode('utf-8'))
+            alias.id, alias.filename.encode('utf-8'))
+
+    def _getBaseURL(self, alias, secure=False):
+        """Get the base URL to use for `alias`.
+
+        :param secure: If true generate https urls on unique domains for
+            security.
+        """
+        if not secure:
+            return self.download_url
+
+        # Secure url generation is the same for both restricted and
+        # unrestricted files aliases : it is used to give web clients (not
+        # appservers) a url to use to access a file which is either
+        # restricted (and so they will also need a TimeLimitedToken) or
+        # is suspected hostile (and so it should be isolated on its own
+        # domain). Note that only the former is currently used in LP.
+        # The algorithm is:
+        # parse the url
+        download_url = config.librarian.download_url
+        parsed = list(urlparse(download_url))
+        # Force the scheme to https
+        parsed[0] = 'https'
+        # Insert the alias id (which is a unique key, thus unique) in the
+        # netloc
+        parsed[1] = ('i%d.restricted.' % alias.id) + parsed[1]
+        return urlunparse(parsed)
 
     def getURLForAlias(self, aliasID, secure=False):
         """Returns the url for talking to the librarian about the given
         alias.
 
         :param aliasID: A unique ID for the alias
-        :param secure: If true generate https urls on unique domains for 
-            security.
-        :returns: String URL, or None if the file has expired and been deleted.
-        """
-        # Note that the path is the same for both secure and insecure URLs - 
-        # this is deliberate: the server doesn't need to know about the original
-        # Host the client provides, testing is easier as we don't need wildcard
-        # https environments on dev machines.
-        path = self._getPathForAlias(aliasID, secure=secure)
-        if path is None:
-            return None
-        if not secure:
-            base = self.download_url
-        else:
-            # Secure url generation is the same for both restricted and
-            # unrestricted files aliases : it is used to give web clients (not
-            # appservers) a url to use to access a file which is either
-            # restricted (and so they will also need a TimeLimitedToken) or
-            # is suspected hostile (and so it should be isolated on its own
-            # domain). Note that only the former is currently used in LP.
-            # The algorithm is: 
-            # parse the url 
-            download_url = config.librarian.download_url
-            parsed = list(urlparse(download_url))
-            # Force the scheme to https
-            parsed[0] = 'https'
-            # Insert the alias id (which is a unique key, thus unique) in the
-            # netloc
-            parsed[1] = ('i%d.restricted.' % aliasID) + parsed[1]
-            base = urlunparse(parsed)
-        return urljoin(base, path)
+        :param secure: If true generate https urls on unique domains for
+            security.
+        :returns: String URL, or None if the file has expired and been
+            deleted.
+        """
+        alias = self._getAlias(aliasID, secure=secure)
+        return self.getURLForAliasObject(alias, secure=secure)
+
+    def getURLForAliasObject(self, alias, secure=False):
+        """Return the download URL for a `LibraryFileAlias`.
+
+        There is a separate `getURLForAlias` that takes an alias ID.  If
+        you're not sure whether it's safe for the client to access your
+        `alias`, use `getURLForAlias` which will retrieve its own copy.
+
+        :param alias: A `LibraryFileAlias` whose URL you want.
+        :param secure: If true generate https urls on unique domains for
+            security.
+        :returns: String URL, or None if the file has expired and been
+            deleted.
+        """
+        # Note that the path is the same for both secure and insecure
+        # URLs.  This is deliberate: the server doesn't need to know
+        # about the original Host the client provides, and testing is
+        # easier as we don't need wildcard https environments on dev
+        # machines.
+        self._checkAliasAccess(alias, secure=secure)
+        base = self._getBaseURL(alias, secure=secure)
+        path = self._getPathForAliasObject(alias)
+        return compose_url(base, path)
 
     def _getURLForDownload(self, aliasID):
         """Returns the internal librarian URL for the alias.
 
         :param aliasID: A unique ID for the alias
 
-        :returns: String URL, or None if the file has expired and been deleted.
+        :returns: String URL, or None if the file has expired and been
+            deleted.
         """
-        path = self._getPathForAlias(aliasID)
-        if path is None:
-            return None
-        base = self._internal_download_url
-        return urljoin(base, path)
+        return compose_url(
+            self._internal_download_url, self._getPathForAlias(aliasID))
 
     def getFileByAlias(
         self, aliasID, timeout=LIBRARIAN_SERVER_DEFAULT_TIMEOUT):
@@ -401,7 +464,7 @@
                 #
                 # Note that URLError is a base class of HTTPError.
                 if isinstance(error, urllib2.HTTPError) and error.code == 404:
-                    raise LookupError, aliasID
+                    raise LookupError(aliasID)
                 # HTTPErrors with a 5xx error code ("server problem")
                 # are a reason to retry the access again, as well as
                 # generic, non-HTTP, URLErrors like "connection refused".
@@ -413,7 +476,7 @@
                         time.sleep(1)
                     else:
                         # There's a test (in
-                        # lib/c/l/browser/tests/test_librarian.py) which 
+                        # lib/c/l/browser/tests/test_librarian.py) which
                         # simulates a librarian server error by raising this
                         # exception, so if you change the exception raised
                         # here, make sure you update the test.
@@ -446,7 +509,6 @@
                                   config.librarian.download_port)
 
 
-
 class RestrictedLibrarianClient(LibrarianClient):
     """See `IRestrictedLibrarianClient`."""
     implements(IRestrictedLibrarianClient)

=== modified file 'lib/canonical/librarian/ftests/test_web.py'
--- lib/canonical/librarian/ftests/test_web.py	2010-09-06 04:52:33 +0000
+++ lib/canonical/librarian/ftests/test_web.py	2010-09-17 01:08:50 +0000
@@ -23,7 +23,6 @@
 from canonical.librarian.client import (
     get_libraryfilealias_download_path,
     LibrarianClient,
-    RestrictedLibrarianClient,
     )
 from canonical.librarian.interfaces import DownloadFailed
 from canonical.launchpad.database import LibraryFileAlias
@@ -42,6 +41,7 @@
     # 500-error issue).
 
     def commit(self):
+        """Synchronize database state."""
         flush_database_updates()
         transaction.commit()
 
@@ -144,23 +144,20 @@
         old_url = 'http://%s:%d/42/%d/%s' % (
                 config.librarian.download_host,
                 config.librarian.download_port,
-                aid, filename
-                )
+                aid, filename)
         self.assertEqual(urlopen(old_url).read(), 'sample')
 
         # If the content id is not an integer, a 404 is raised
         old_url = 'http://%s:%d/foo/%d/%s' % (
                 config.librarian.download_host,
                 config.librarian.download_port,
-                aid, filename
-                )
+                aid, filename)
         self.require404(self._makeURL(aid, 'different.txt'))
 
     def _makeURL(self, aliasID, filename):
         host = config.librarian.download_host
         port = config.librarian.download_port
-        return 'http://%s:%d/%d/%s' % (
-                host, port, aliasID, filename)
+        return 'http://%s:%d/%d/%s' % (host, port, aliasID, filename)
 
     def test_404(self):
         client = LibrarianClient()
@@ -231,18 +228,18 @@
             last_modified_header, 'Tue, 30 Jan 2001 13:45:59 GMT')
 
     def get_restricted_file_and_public_url(self):
-        # Use a regular LibrarianClient to ensure we speak to the nonrestricted
-        # port on the librarian which is where secured restricted files are
-        # served from.
+        # Use a regular LibrarianClient to ensure we speak to the
+        # nonrestricted port on the librarian which is where secured
+        # restricted files are served from.
         client = LibrarianClient()
-        fileAlias = client.addFile('sample', 12, StringIO('a'*12),
-            contentType='text/plain')
+        fileAlias = client.addFile(
+            'sample', 12, StringIO('a'*12), contentType='text/plain')
         # Note: We're deliberately using the wrong url here: we should be
-        # passing secure=True to getURLForAlias, but to use the returned URL we
-        # would need a wildcard DNS facility patched into urlopen; instead
-        # we use the *deliberate* choice of having the path of secure and insecure
-        # urls be the same, so that we can test it: the server code doesn't need
-        # to know about the fancy wildcard domains.
+        # passing secure=True to getURLForAlias, but to use the returned URL
+        # we would need a wildcard DNS facility patched into urlopen; instead
+        # we use the *deliberate* choice of having the path of secure and
+        # insecure urls be the same, so that we can test it: the server code
+        # doesn't need to know about the fancy wildcard domains.
         url = client.getURLForAlias(fileAlias)
         # Now that we have a url which talks to the public librarian, make the
         # file restricted.
@@ -315,7 +312,7 @@
 
     def test_restricted_with_token(self):
         fileAlias, url = self.get_restricted_file_and_public_url()
-        # We have the base url for a restricted file; grant access to it 
+        # We have the base url for a restricted file; grant access to it
         # for a short time.
         token = TimeLimitedToken.allocate(url)
         url = url + "?token=%s" % token
@@ -328,10 +325,10 @@
 
     def test_restricted_with_expired_token(self):
         fileAlias, url = self.get_restricted_file_and_public_url()
-        # We have the base url for a restricted file; grant access to it 
+        # We have the base url for a restricted file; grant access to it
         # for a short time.
         token = TimeLimitedToken.allocate(url)
-        # But time has passed 
+        # But time has passed
         store = session_store()
         tokens = store.find(TimeLimitedToken, TimeLimitedToken.token==token)
         tokens.set(
@@ -363,6 +360,7 @@
         # Perhaps we should also set Expires to the Last-Modified.
 
     def require404(self, url):
+        """Assert that opening `url` raises a 404."""
         try:
             urlopen(url)
             self.fail('404 not raised')
@@ -400,7 +398,7 @@
         # (otherwise, depending on the resolution of clocks and things,
         # an immediate access might not look any newer).
         LibraryFileAlias.get(id1).last_accessed = datetime(
-            2004,1,1,12,0,0, tzinfo=pytz.timezone('Australia/Sydney'))
+            2004, 1, 1, 12, 0, 0, tzinfo=pytz.timezone('Australia/Sydney'))
         self.commit()
 
         # Check that last_accessed is updated when the file is accessed
@@ -414,6 +412,20 @@
 
         self.failUnless(access_time_1 < access_time_2)
 
+    def test_getURLForAliasObject(self):
+        # getURLForAliasObject returns the same URL as getURLForAlias.
+        client = LibrarianClient()
+        content = "Test content"
+        alias_id = client.addFile(
+            'test.txt', len(content), StringIO(content),
+            contentType='text/plain')
+        self.commit()
+
+        alias = getUtility(ILibraryFileAliasSet)[alias_id]
+        self.assertEqual(
+            client.getURLForAlias(alias_id),
+            client.getURLForAliasObject(alias))
+
 
 class DeletedContentTestCase(unittest.TestCase):
 
@@ -428,8 +440,7 @@
         LaunchpadZopelessLayer.switchDbUser('testadmin')
 
         alias = getUtility(ILibraryFileAliasSet).create(
-                'whatever', 8, StringIO('xxx\nxxx\n'), 'text/plain'
-                )
+                'whatever', 8, StringIO('xxx\nxxx\n'), 'text/plain')
         alias_id = alias.id
         transaction.commit()
 
@@ -451,8 +462,7 @@
         cur = cursor()
         cur.execute("""
             UPDATE LibraryFileAlias SET content=NULL WHERE id=%s
-            """, (alias.id,)
-            )
+            """, (alias.id, ))
         transaction.commit()
 
         # Things become not found
@@ -465,7 +475,3 @@
             self.fail('404 not raised')
         except HTTPError, x:
             self.failUnlessEqual(x.code, 404)
-
-
-def test_suite():
-    return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/canonical/librarian/interfaces.py'
--- lib/canonical/librarian/interfaces.py	2010-09-05 10:49:21 +0000
+++ lib/canonical/librarian/interfaces.py	2010-09-17 01:08:50 +0000
@@ -103,6 +103,15 @@
             separately be obtained and combined with the URL to use it.
         """
 
+    def getURLForAliasObject(alias):
+        """Returns the URL to a given `LibraryFileAlias` object.
+
+        Use this with care.  Do not pass the `LibraryFileAlias` object
+        across process or thread boundaries.  If you need to pass a
+        `LibraryFileAlias` across a boundary, pass alias.id and use
+        `getURLForAlias` instead.
+        """
+
     def getFileByAlias(aliasID, timeout=LIBRARIAN_SERVER_DEFAULT_TIMEOUT):
         """Returns a file-like object to read the file contents from.
 

=== modified file 'lib/canonical/testing/ftests/test_mockdb.py'
--- lib/canonical/testing/ftests/test_mockdb.py	2010-01-11 18:06:23 +0000
+++ lib/canonical/testing/ftests/test_mockdb.py	2010-09-17 01:08:50 +0000
@@ -42,7 +42,7 @@
         for con in self.connections:
             try:
                 con.close()
-            except:
+            except Exception:
                 pass
         self.connections = []
 

=== modified file 'lib/canonical/testing/layers.py'
--- lib/canonical/testing/layers.py	2010-09-06 09:41:01 +0000
+++ lib/canonical/testing/layers.py	2010-09-17 01:08:50 +0000
@@ -413,11 +413,10 @@
         """
         test_result = BaseLayer.getCurrentTestResult()
         if test_result.wasSuccessful():
-            # pylint: disable-msg=W0702
             test_case = BaseLayer.getCurrentTestCase()
             try:
                 raise LayerIsolationError(message)
-            except:
+            except LayerIsolationError:
                 test_result.addError(test_case, sys.exc_info())
 
     @classmethod

=== modified file 'lib/lp/answers/browser/questiontarget.py'
--- lib/lp/answers/browser/questiontarget.py	2010-08-31 11:31:04 +0000
+++ lib/lp/answers/browser/questiontarget.py	2010-09-17 01:08:50 +0000
@@ -78,6 +78,7 @@
     IQuestionTarget,
     ISearchQuestionsForm,
     )
+from lp.app.enums import service_uses_launchpad
 from lp.app.errors import NotFoundError
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.product import IProduct
@@ -204,7 +205,7 @@
             return self.default_template
         involvement = getMultiAdapter(
             (self.context, self.request), name='+get-involved')
-        if involvement.official_answers:
+        if service_uses_launchpad(involvement.answers_usage):
             # Primary contexts that officially use answers have a
             # search and listing presentation.
             return self.default_template

=== modified file 'lib/lp/answers/doc/question.txt'
--- lib/lp/answers/doc/question.txt	2010-07-28 16:56:05 +0000
+++ lib/lp/answers/doc/question.txt	2010-09-17 01:08:50 +0000
@@ -72,13 +72,13 @@
 ==============
 
 A product or distribution may be officially supported by the community using
-the Answer Tracker.  This status is set by the official_answers attribute on
+the Answer Tracker.  This status is set by the answers_usage attribute on
 the IProduct and IDistribution.
 
-    >>> ubuntu.official_answers
-    True
-    >>> firefox.official_answers
-    True
+    >>> print ubuntu.answers_usage.name
+    LAUNCHPAD
+    >>> print firefox.answers_usage.name
+    LAUNCHPAD
 
 
 IQuestion interface

=== modified file 'lib/lp/answers/doc/questionsets.txt'
--- lib/lp/answers/doc/questionsets.txt	2010-07-27 17:18:24 +0000
+++ lib/lp/answers/doc/questionsets.txt	2010-09-17 01:08:50 +0000
@@ -208,14 +208,14 @@
 order of the returned projects is based on the number of questions asked
 during the period.
 
-    >>> ubuntu.official_answers
-    True
-    >>> firefox.official_answers
-    True
-    >>> landscape.official_answers
-    False
-    >>> launchpad.official_answers
-    True
+    >>> print ubuntu.answers_usage.name
+    LAUNCHPAD
+    >>> print firefox.answers_usage.name
+    LAUNCHPAD
+    >>> print landscape.answers_usage.name
+    UNKNOWN
+    >>> print launchpad.answers_usage.name
+    LAUNCHPAD
 
     # Launchpad is not returned because the question was not asked in
     # the last 60 days.

=== modified file 'lib/lp/app/enums.py'
--- lib/lp/app/enums.py	2010-08-20 20:31:18 +0000
+++ lib/lp/app/enums.py	2010-09-17 01:08:50 +0000
@@ -1,11 +1,12 @@
 # Copyright 2010 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-"""Enumerations used in the lp/app modules."""
+"""Enumerations and related utilities used in the lp/app modules."""
 
 __metaclass__ = type
 __all__ = [
     'ServiceUsage',
+    'service_uses_launchpad',
     ]
 
 from lazr.enum import (
@@ -45,3 +46,7 @@
 
     The pillar does not use this type of service in Launchpad or externally.
     """)
+
+
+def service_uses_launchpad(usage_enum):
+    return usage_enum == ServiceUsage.LAUNCHPAD

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2010-09-01 12:47:32 +0000
+++ lib/lp/bugs/configure.zcml	2010-09-17 01:08:50 +0000
@@ -10,6 +10,8 @@
     xmlns:lp="http://namespaces.canonical.com/lp";
     i18n_domain="launchpad">
 
+  <authorizations module=".security" />
+
   <include package=".browser"/>
 
   <publisher

=== modified file 'lib/lp/bugs/doc/bugsubscription.txt'
--- lib/lp/bugs/doc/bugsubscription.txt	2010-08-23 09:18:51 +0000
+++ lib/lp/bugs/doc/bugsubscription.txt	2010-09-17 01:08:50 +0000
@@ -863,7 +863,7 @@
     ...     print '%s (%s)' % (
     ...         subscription.person.displayname,
     ...         subscription.display_subscribed_by)
-    Mark Shuttleworth (Subscribed themselves)
+    Mark Shuttleworth (Self-subscribed)
     Robert Collins (Subscribed by Mark Shuttleworth)
     >>> params = CreateBugParams(
     ...     title="one more dupe test bug",
@@ -877,6 +877,6 @@
     >>> for subscription in ff_bug.getSubscriptionsFromDuplicates():
     ...     print '%s (%s)' % (
     ...         subscription.person.displayname,
-    ...         subscription.display_subscribed_by)
-    Foo Bar (Subscribed by Robert Collins)
-    Scott James Remnant (Subscribed themselves)
+    ...         subscription.display_duplicate_subscribed_by)
+    Foo Bar (Subscribed to bug 28 by Robert Collins)
+    Scott James Remnant (Self-subscribed to bug 28)

=== modified file 'lib/lp/bugs/interfaces/bugsubscription.py'
--- lib/lp/bugs/interfaces/bugsubscription.py	2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/interfaces/bugsubscription.py	2010-09-17 01:08:50 +0000
@@ -47,6 +47,7 @@
         "e-mail address.")))
     bug = exported(Reference(
         IBug, title=_("Bug"), required=True, readonly=True))
+    bugID = Int(title=u"The bug id.", readonly=True)
     bug_notification_level = Choice(
         title=_("Bug notification level"), required=True,
         vocabulary=BugNotificationLevel,
@@ -64,6 +65,9 @@
     display_subscribed_by = Attribute(
         "`subscribed_by` formatted for display.")
 
+    display_duplicate_subscribed_by = Attribute(
+        "duplicate bug `subscribed_by` formatted for display.")
+
     @call_with(user=REQUEST_USER)
     @export_read_operation()
     def canBeUnsubscribedByUser(user):

=== modified file 'lib/lp/bugs/model/bugsubscription.py'
--- lib/lp/bugs/model/bugsubscription.py	2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/model/bugsubscription.py	2010-09-17 01:08:50 +0000
@@ -46,10 +46,19 @@
     def display_subscribed_by(self):
         """See `IBugSubscription`."""
         if self.person == self.subscribed_by:
-            return u'Subscribed themselves'
+            return u'Self-subscribed'
         else:
             return u'Subscribed by %s' % self.subscribed_by.displayname
 
+    @property
+    def display_duplicate_subscribed_by(self):
+        """See `IBugSubscription`."""
+        if self.person == self.subscribed_by:
+            return u'Self-subscribed to bug %s' % (self.bugID)
+        else:
+            return u'Subscribed to bug %s by %s' % (self.bugID,
+                self.subscribed_by.displayname)
+
     def canBeUnsubscribedByUser(self, user):
         """See `IBugSubscription`."""
         if user is None:

=== added file 'lib/lp/bugs/security.py'
--- lib/lp/bugs/security.py	1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/security.py	2010-09-17 01:08:50 +0000
@@ -0,0 +1,173 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the bugs module."""
+
+__metaclass__ = type
+__all__ = []
+
+from canonical.launchpad.interfaces.launchpad import IHasBug
+from canonical.launchpad.interfaces.message import IMessage
+from canonical.launchpad.security import (
+    AnonymousAuthorization,
+    AuthorizationBase,
+    )
+from lp.bugs.interfaces.bug import IBug
+from lp.bugs.interfaces.bugattachment import IBugAttachment
+from lp.bugs.interfaces.bugbranch import IBugBranch
+from lp.bugs.interfaces.bugnomination import IBugNomination
+from lp.bugs.interfaces.bugsubscription import IBugSubscription
+from lp.bugs.interfaces.bugtracker import IBugTracker
+from lp.bugs.interfaces.bugwatch import IBugWatch
+
+
+class EditBugNominationStatus(AuthorizationBase):
+    permission = 'launchpad.Driver'
+    usedfor = IBugNomination
+
+    def checkAuthenticated(self, user):
+        return self.obj.canApprove(user.person)
+
+
+class EditBugTask(AuthorizationBase):
+    """Permission checker for editing objects linked to a bug.
+
+    Allow any logged-in user to edit objects linked to public
+    bugs. Allow only explicit subscribers to edit objects linked to
+    private bugs.
+    """
+    permission = 'launchpad.Edit'
+    usedfor = IHasBug
+
+    def checkAuthenticated(self, user):
+        # Delegated entirely to the bug.
+        return self.obj.bug.userCanView(user)
+
+
+class PublicToAllOrPrivateToExplicitSubscribersForBugTask(AuthorizationBase):
+    permission = 'launchpad.View'
+    usedfor = IHasBug
+
+    def checkAuthenticated(self, user):
+        return self.obj.bug.userCanView(user.person)
+
+    def checkUnauthenticated(self):
+        """Allow anonymous users to see non-private bugs only."""
+        return not self.obj.bug.private
+
+
+class EditPublicByLoggedInUserAndPrivateByExplicitSubscribers(
+    AuthorizationBase):
+    permission = 'launchpad.Edit'
+    usedfor = IBug
+
+    def checkAuthenticated(self, user):
+        """Allow any logged in user to edit a public bug, and only
+        explicit subscribers to edit private bugs. Any bug that can be seen can
+        be edited.
+        """
+        return self.obj.userCanView(user)
+
+    def checkUnauthenticated(self):
+        """Never allow unauthenticated users to edit a bug."""
+        return False
+
+
+class PublicToAllOrPrivateToExplicitSubscribersForBug(AuthorizationBase):
+    permission = 'launchpad.View'
+    usedfor = IBug
+
+    def checkAuthenticated(self, user):
+        """Allow any user to see non-private bugs, but only explicit
+        subscribers to see private bugs.
+        """
+        return self.obj.userCanView(user.person)
+
+    def checkUnauthenticated(self):
+        """Allow anonymous users to see non-private bugs only."""
+        return not self.obj.private
+
+
+class EditBugBranch(EditPublicByLoggedInUserAndPrivateByExplicitSubscribers):
+    permission = 'launchpad.Edit'
+    usedfor = IBugBranch
+
+    def __init__(self, bug_branch):
+        # The same permissions as for the BugBranch's bug should apply
+        # to the BugBranch itself.
+        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
+            self, bug_branch.bug)
+
+
+class ViewBugAttachment(PublicToAllOrPrivateToExplicitSubscribersForBug):
+    """Security adapter for viewing a bug attachment.
+
+    If the user is authorized to view the bug, he's allowed to view the
+    attachment.
+    """
+    permission = 'launchpad.View'
+    usedfor = IBugAttachment
+
+    def __init__(self, bugattachment):
+        PublicToAllOrPrivateToExplicitSubscribersForBug.__init__(
+            self, bugattachment.bug)
+
+
+class EditBugAttachment(
+    EditPublicByLoggedInUserAndPrivateByExplicitSubscribers):
+    """Security adapter for editing a bug attachment.
+
+    If the user is authorized to view the bug, he's allowed to edit the
+    attachment.
+    """
+    permission = 'launchpad.Edit'
+    usedfor = IBugAttachment
+
+    def __init__(self, bugattachment):
+        EditPublicByLoggedInUserAndPrivateByExplicitSubscribers.__init__(
+            self, bugattachment.bug)
+
+
+class ViewBugSubscription(AnonymousAuthorization):
+
+    usedfor = IBugSubscription
+
+
+class ViewBugMessage(AnonymousAuthorization):
+
+    usedfor = IMessage
+
+
+class ViewBugTracker(AnonymousAuthorization):
+    """Anyone can view a bug tracker."""
+    usedfor = IBugTracker
+
+
+class EditBugTracker(AuthorizationBase):
+    permission = 'launchpad.Edit'
+    usedfor = IBugTracker
+
+    def checkAuthenticated(self, user):
+        """Any logged-in user can edit a bug tracker."""
+        return True
+
+
+class AdminBugTracker(AuthorizationBase):
+    permission = 'launchpad.Admin'
+    usedfor = IBugTracker
+
+    def checkAuthenticated(self, user):
+        return (
+            user.in_janitor or
+            user.in_admin or
+            user.in_launchpad_developers)
+
+
+class AdminBugWatch(AuthorizationBase):
+    permission = 'launchpad.Admin'
+    usedfor = IBugWatch
+
+    def checkAuthenticated(self, user):
+        return (
+            user.in_admin or
+            user.in_launchpad_developers)

=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt'
--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt	2009-11-06 20:28:16 +0000
+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions.txt	2010-09-17 01:08:50 +0000
@@ -41,7 +41,7 @@
   >>> browser.open(
   ...     'http://bugs.launchpad.dev/bugs/1/+bug-portlet-subscribers-content')
   >>> print_direct_subscribers(browser.contents)
-  Foo Bar (Subscribed themselves) (Unsubscribe Foo Bar)
+  Foo Bar (Self-subscribed) (Unsubscribe Foo Bar)
   Sample Person (Subscribed by Launchpad Janitor)
   Steve Alexander (Subscribed by Launchpad Janitor)
 
@@ -208,7 +208,7 @@
   ...     "http://launchpad.dev/bugs/3/+bug-portlet-dupe-subscribers-content";)
   >>> print_subscribers_from_duplicates(stevea_browser.contents)
   From duplicates:
-  Steve Alexander (Subscribed by Launchpad Janitor)
+  Steve Alexander (Subscribed to bug 2 by Launchpad Janitor)
                   (Unsubscribe Steve Alexander)
 
   >>> stevea_browser.getLink(id='unsubscribe-subscriber-11').mech_link.url
@@ -267,7 +267,7 @@
   From duplicates:
   Sample Person (Subscribed ...)
   Steve Alexander (Subscribed ...) (Unsubscribe Steve Alexander)
-  testing Spanish team (Subscribed by Foo Bar)
+  testing Spanish team (Subscribed to bug 1 by Foo Bar)
 
   >>> stevea_browser.open(
   ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)
@@ -309,7 +309,7 @@
 
   >>> print_subscribers_from_duplicates(foobar_browser.contents)
   From duplicates:
-  Ubuntu Team (Subscribed by Foo Bar) (Unsubscribe Ubuntu Team)
+  Ubuntu Team (Subscribed to bug 2 by Foo Bar) (Unsubscribe Ubuntu Team)
 
   >>> foobar_browser.open(
   ...     "http://launchpad.dev/bugs/3/+bug-portlet-subscribers-content";)

=== modified file 'lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt'
--- lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt	2010-02-09 12:13:43 +0000
+++ lib/lp/bugs/stories/bugs/xx-front-page-bug-lists.txt	2010-09-17 01:08:50 +0000
@@ -41,28 +41,50 @@
     </span>
 
 The list of recently reported bugs contains up to the last 5 bugs reported
-across Launchpad.
+across Launchpad. The text for each bug contains links to the bug itself,
+to the bug target and to the bug reporter's page.
 
+    >>> def print_bugs_links(bug_row):
+    ...     icon_td, summary_td, = bug_row('td')
+    ...     print "%s: %s" % (
+    ...         summary_td.b.a.renderContents().strip(),
+    ...         summary_td.a.renderContents())
+    ...     bug_link, bug_target_link, bug_reporter_link = summary_td('a')
+    ...     print bug_link['href']
+    ...     print bug_target_link['href']
+    ...     print bug_reporter_link['href']
     >>> for tr in reported_bugs('tr'):
-    ...     icon_td, summary_td, = tr('td')
-    ...     print "%s: %s" % (
-    ...         summary_td.b.renderContents().strip(),
-    ...         summary_td.a.renderContents())
-    Bigfixer:
-             Summary for new bug ...
-    <BLANKLINE>
-    Bigfixer:
-             Summary for new bug ...
-    <BLANKLINE>
-    Bigfixer:
-             Summary for new bug ...
-    <BLANKLINE>
-    Bigfixer:
-             Summary for new bug ...
-    <BLANKLINE>
-    Bigfixer:
-             Summary for new bug ...
-    <BLANKLINE>
+    ...     print_bugs_links(tr)
+    Bigfixer:
+             Summary for new bug ...
+    <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
+    Bigfixer:
+             Summary for new bug ...
+    <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
+    Bigfixer:
+             Summary for new bug ...
+    <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
+    Bigfixer:
+             Summary for new bug ...
+    <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
+    Bigfixer:
+             Summary for new bug ...
+    <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
 
 
 Fixed bugs
@@ -84,22 +106,34 @@
 across Launchpad.
 
     >>> for tr in fixed_bugs('tr'):
-    ...     icon_td, summary_td, = tr('td')
-    ...     print "%s: %s" % (
-    ...         summary_td.b.renderContents().strip(),
-    ...         summary_td.a.renderContents())
+    ...     print_bugs_links(tr)
     Bigfixer:
              Summary for new bug 10
     <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
     Bigfixer:
              Summary for new bug 9
     <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
     Bigfixer:
              Summary for new bug 8
     <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
     Bigfixer:
              Summary for new bug 7
     <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...
     Bigfixer:
              Summary for new bug 6
     <BLANKLINE>
+    /bugs/...
+    /bigfixer
+    http://launchpad.dev/~person-name...

=== modified file 'lib/lp/bugs/templates/bug-listing-detailed.pt'
--- lib/lp/bugs/templates/bug-listing-detailed.pt	2010-01-12 14:13:26 +0000
+++ lib/lp/bugs/templates/bug-listing-detailed.pt	2010-09-17 01:08:50 +0000
@@ -10,9 +10,9 @@
       </a>
     </div>
     <div class="lesser">
-      in 
+      in
         <tal:per_pillar repeat="pillar context/affected_pillars">
-          <b tal:content="pillar/displayname" />,
+          <b tal:content="structure pillar/fmt:link:bugs" />,
         </tal:per_pillar>
       reported
       <span

=== modified file 'lib/lp/bugs/templates/bug-portlet-dupe-subscribers-content.pt'
--- lib/lp/bugs/templates/bug-portlet-dupe-subscribers-content.pt	2009-11-05 19:01:12 +0000
+++ lib/lp/bugs/templates/bug-portlet-dupe-subscribers-content.pt	2010-09-17 01:08:50 +0000
@@ -20,8 +20,30 @@
         id string:dupe-${subscription/css_name};
       "
     >
-      <metal:subscriber
-          metal:use-macro="bug/@@+bug-portlet-subscribers-content/subscriber-row" />
+
+            <a
+               tal:condition="subscription/person/name|nothing"
+               tal:attributes="
+                 href subscription/person/fmt:url;
+                 title subscription/display_duplicate_subscribed_by;
+                 name subscription/person/fmt:displayname
+               "
+            >
+              <tal:block replace="structure subscription/person/fmt:icon" />
+              <tal:block replace="subscription/person/fmt:displayname/fmt:shorten/20" />
+            </a>
+
+            <a tal:condition="python: subscription.canBeUnsubscribedByUser(view.user)"
+               href="+subscribe"
+               tal:attributes="
+                 title string:Unsubscribe ${subscription/person/fmt:displayname};
+                 id string:unsubscribe-${subscription/css_name};
+                 class python: view.getSubscriptionClassForUser(subscription.person)
+               "
+            >
+              <img class="unsub-icon" src="/@@/remove"
+                tal:attributes="id string:unsubscribe-icon-${subscription/css_name}" />
+            </a>
     </div>
   </div>
 </div>

=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py	2010-08-25 16:25:02 +0000
+++ lib/lp/buildmaster/interfaces/builder.py	2010-09-17 01:08:50 +0000
@@ -147,7 +147,7 @@
                       'buildd-slave, e.g.: foobar-host.ppa'))
 
     active = Bool(
-        title=_('Active'), required=True, default=True,
+        title=_('Publicly Visible'), required=True, default=True,
         description=_('Whether or not to present the builder publicly.'))
 
     slave = Attribute("xmlrpclib.Server instance corresponding to builder.")

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2010-08-23 02:07:45 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2010-09-17 01:08:50 +0000
@@ -16,6 +16,7 @@
 
 
 from bzrlib.plugins.builder.recipe import (
+    ForbiddenInstructionError,
     RecipeParseError,
     RecipeParser,
     )
@@ -57,7 +58,6 @@
 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
 from lp.code.errors import (
     BuildAlreadyPending,
-    ForbiddenInstruction,
     NoSuchBranch,
     PrivateBranchRecipe,
     )
@@ -326,16 +326,14 @@
 
     @action('Create Recipe', name='create')
     def request_action(self, action, data):
-        parser = RecipeParser(data['recipe_text'])
-        recipe = parser.parse()
         try:
             source_package_recipe = getUtility(
                 ISourcePackageRecipeSource).new(
-                    self.user, data['owner'], data['name'], recipe,
-                    data['description'], data['distros'],
+                    self.user, data['owner'], data['name'],
+                    data['recipe_text'], data['description'], data['distros'],
                     data['daily_build_archive'], data['build_daily'])
             Store.of(source_package_recipe).flush()
-        except ForbiddenInstruction:
+        except ForbiddenInstructionError:
             # XXX: bug=592513 We shouldn't be hardcoding "run" here.
             self.setFieldError(
                 'recipe_text',
@@ -397,9 +395,9 @@
         recipe = parser.parse()
         if self.context.builder_recipe != recipe:
             try:
-                self.context.builder_recipe = recipe
+                self.context.setRecipeText(recipe_text)
                 changed = True
-            except ForbiddenInstruction:
+            except ForbiddenInstructionError:
                 # XXX: bug=592513 We shouldn't be hardcoding "run" here.
                 self.setFieldError(
                     'recipe_text',

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-08-27 02:11:36 +0000
+++ lib/lp/code/configure.zcml	2010-09-17 01:08:50 +0000
@@ -642,17 +642,6 @@
     <allow interface="lp.code.interfaces.branchrevision.IBranchRevision"/>
   </class>
 
-  <!-- BranchRevisionSet -->
-
-  <class class="lp.code.model.branchrevision.BranchRevisionSet">
-    <allow interface="lp.code.interfaces.branchrevision.IBranchRevisionSet"/>
-  </class>
-  <securedutility
-      class="lp.code.model.branchrevision.BranchRevisionSet"
-      provides="lp.code.interfaces.branchrevision.IBranchRevisionSet">
-    <allow interface="lp.code.interfaces.branchrevision.IBranchRevisionSet"/>
-  </securedutility>
-
   <!-- CodeReviewComment -->
 
   <class class="lp.code.model.codereviewcomment.CodeReviewComment">
@@ -960,28 +949,23 @@
   <!-- SourcePackageRecipeData -->
   <class
      class="lp.code.model.sourcepackagerecipedata.SourcePackageRecipeData">
-    <allow
+    <require permission="launchpad.View"
     interface="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeData"/>
   </class>
   <!-- SourcePackageRecipe -->
   <class
      class="lp.code.model.sourcepackagerecipe.SourcePackageRecipe">
     <require permission="launchpad.View"
-      interface="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe"
+      interface="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeView
+                 lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeEditableAttributes
+                 lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeData"
       />
     <require
        permission="launchpad.Edit"
-       set_attributes="
-           build_daily
-           builder_recipe
-           daily_build_archive
-           date_last_modified
-           description
-           distroseries
-           name
-           owner
-           sourcepackagename
-           is_stale"/>
+       interface="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeEdit"
+       set_schema="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipeEditableAttributes"
+       set_attributes="builder_recipe date_last_modified"
+        />
   </class>
   <class
      class="bzrlib.plugins.builder.recipe.BaseRecipeBranch">

=== modified file 'lib/lp/code/doc/revision.txt'
--- lib/lp/code/doc/revision.txt	2010-07-14 14:50:58 +0000
+++ lib/lp/code/doc/revision.txt	2010-09-17 01:08:50 +0000
@@ -130,7 +130,7 @@
 In particular, IBranch.getScannerData efficiently retrieves the BranchRevision
 data needed by the branch-scanner script.
 
-    >>> ancestry, history, mapping = branch.getScannerData()
+    >>> ancestry, history = branch.getScannerData()
 
 The first return value is a set of revision_id strings for the full ancestry
 of the branch.
@@ -158,19 +158,6 @@
     foo@localhost-20051031170239-5fce7d6bd3f01efc
     foo@localhost-20051031170357-1301ad6d387feb23
 
-The third return value is a mapping from revision_id strings to database ids
-integers of the corresponding BranchRevision rows for this branch.
-
-    >>> for revision_id, db_id in sorted(mapping.iteritems()):
-    ...     print revision_id, db_id
-    foo@localhost-20051031165758-48acedf2b6a2e898 12
-    foo@localhost-20051031170008-098959758bf79803 13
-    foo@localhost-20051031170239-5fce7d6bd3f01efc 14
-    foo@localhost-20051031170357-1301ad6d387feb23 15
-    test@xxxxxxxxxxxxx-20051031165248-6f1bb97973c2b4f4 10
-    test@xxxxxxxxxxxxx-20051031165338-5f2f3d6b10bb3bf0 11
-    test@xxxxxxxxxxxxx-20051031165532-3113df343e494daa 18
-    test@xxxxxxxxxxxxx-20051031165901-43b9644ec2eacc4e 19
 
 === Deleting BranchRevisions ===
 
@@ -180,7 +167,7 @@
 of the branch, then some of BranchRevision records will need to be
 removed.
 
-BranchRevision records are deleted using the `BranchRevisionSet.delete`
+BranchRevision records are deleted using the `Branch.removeBranchRevisions`
 method.
 
 
@@ -197,14 +184,14 @@
     6
     >>> revno_6.branch == branch
     True
-    >>> print revno_6.revision.revision_id
+    >>> rev_id = revno_6.revision.revision_id
+    >>> print rev_id
     foo@localhost-20051031170357-1301ad6d387feb23
 
 We remove the last revision from the branch. This is similar to what
 "bzr uncommit" does.
 
-    >>> from lp.code.interfaces.branchrevision import IBranchRevisionSet
-    >>> getUtility(IBranchRevisionSet).delete(revno_6.id)
+    >>> branch.removeBranchRevisions(rev_id)
 
 Afterwards, the last commit on the branch has revision number 5.
 

=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py	2010-08-03 03:43:33 +0000
+++ lib/lp/code/errors.py	2010-09-17 01:08:50 +0000
@@ -25,7 +25,6 @@
     'CodeImportAlreadyRunning',
     'CodeImportNotInReviewedState',
     'ClaimReviewFailed',
-    'ForbiddenInstruction',
     'InvalidBranchMergeProposal',
     'InvalidNamespace',
     'NoLinkedBranch',
@@ -242,14 +241,6 @@
     webservice_error(400)
 
 
-class ForbiddenInstruction(Exception):
-    """A forbidden instruction was found in the recipe."""
-
-    def __init__(self, instruction_name):
-        super(ForbiddenInstruction, self).__init__()
-        self.instruction_name = instruction_name
-
-
 class TooNewRecipeFormat(Exception):
     """The format of the recipe supplied was too new."""
 

=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py	2010-09-03 11:54:23 +0000
+++ lib/lp/code/interfaces/branch.py	2010-09-17 01:08:50 +0000
@@ -803,6 +803,12 @@
     def createBranchRevision(sequence, revision):
         """Create a new `BranchRevision` for this branch."""
 
+    def removeBranchRevisions(revision_ids):
+        """Remove the specified revision_ids from this Branch's revisions.
+
+        :param revision_ids: Either a single revision_id or an iterable.
+        """
+
     def createBranchRevisionFromIDs(revision_id_sequence_pairs):
         """Create a batch of BranchRevision objects.
 

=== modified file 'lib/lp/code/interfaces/branchrevision.py'
--- lib/lp/code/interfaces/branchrevision.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/branchrevision.py	2010-09-17 01:08:50 +0000
@@ -6,7 +6,9 @@
 """BranchRevision interfaces."""
 
 __metaclass__ = type
-__all__ = ['IBranchRevision', 'IBranchRevisionSet']
+__all__ = [
+    'IBranchRevision',
+    ]
 
 from zope.interface import (
     Attribute,
@@ -32,10 +34,3 @@
             " None for merged revisions which are not part of the history."))
     branch = Attribute("The branch this revision is included in.")
     revision = Attribute("A revision that is included in the branch.")
-
-
-class IBranchRevisionSet(Interface):
-    """The set of all branch-revision associations."""
-
-    def delete(branch_revision_id):
-        """Delete the BranchRevision."""

=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py	2010-09-17 01:08:50 +0000
@@ -80,21 +80,12 @@
         """An iterator of the branches referenced by this recipe."""
 
 
-class ISourcePackageRecipe(IHasOwner, ISourcePackageRecipeData):
-    """An ISourcePackageRecipe describes how to build a source package.
-
-    More precisely, it describes how to combine a number of branches into a
-    debianized source tree.
-    """
-    export_as_webservice_entry()
+class ISourcePackageRecipeView(Interface):
+    """IBranch attributes that require launchpad.View permission."""
 
     id = Int()
 
-    daily_build_archive = exported(Reference(
-        IArchive, title=_("The archive to use for daily builds.")))
-
     date_created = Datetime(required=True, readonly=True)
-    date_last_modified = Datetime(required=True, readonly=True)
 
     registrant = exported(
         PublicPersonChoice(
@@ -102,6 +93,69 @@
             required=True, readonly=True,
             vocabulary='ValidPersonOrTeam'))
 
+    recipe_text = exported(Text())
+
+    def isOverQuota(requester, distroseries):
+        """True if the recipe/requester/distroseries combo is >= quota.
+
+        :param requester: The Person requesting a build.
+        :param distroseries: The distroseries to build for.
+        """
+
+    def getBuilds(pending=False):
+        """Return a ResultSet of all the builds in the given state.
+
+        :param pending: If True, select all builds that are pending.  If
+            False, select all builds that are not pending.
+        """
+
+    def getLastBuild():
+        """Return the the most recent build of this recipe."""
+
+    @call_with(requester=REQUEST_USER)
+    @operation_parameters(
+        archive=Reference(schema=IArchive),
+        distroseries=Reference(schema=IDistroSeries),
+        pocket=Choice(vocabulary=PackagePublishingPocket,)
+        )
+    @export_write_operation()
+    def requestBuild(archive, distroseries, requester, pocket):
+        """Request that the recipe be built in to the specified archive.
+
+        :param archive: The IArchive which you want the build to end up in.
+        :param requester: the person requesting the build.
+        :param pocket: the pocket that should be targeted.
+        :raises: various specific upload errors if the requestor is not
+            able to upload to the archive.
+        """
+
+
+class ISourcePackageRecipeEdit(Interface):
+    """ISourcePackageRecipe methods that require launchpad.Edit permission."""
+
+    @operation_parameters(recipe_text=Text())
+    @export_write_operation()
+    def setRecipeText(recipe_text):
+        """Set the text of the recipe."""
+
+    def destroySelf():
+        """Remove this SourcePackageRecipe from the database.
+
+        This requires deleting any rows with non-nullable foreign key
+        references to this object.
+        """
+
+class ISourcePackageRecipeEditableAttributes(IHasOwner):
+    """ISourcePackageRecipe attributes that can be edited.
+
+    These attributes need launchpad.View to see, and launchpad.Edit to change.
+    """
+    daily_build_archive = exported(Reference(
+        IArchive, title=_("The archive to use for daily builds.")))
+
+    builder_recipe = Attribute(
+        _("The bzr-builder data structure for the recipe."))
+
     owner = exported(
         PersonChoice(
             title=_('Owner'),
@@ -115,7 +169,6 @@
         readonly=False)
     build_daily = exported(Bool(
         title=_("Build daily")))
-    is_stale = Bool(title=_('Recipe is stale.'))
 
     name = exported(TextLine(
             title=_("Name"), required=True,
@@ -126,61 +179,24 @@
         title=_('Description'), required=True,
         description=_('A short description of the recipe.'))
 
-    builder_recipe = Attribute(
-        _("The bzr-builder data structure for the recipe."))
-
+    date_last_modified = Datetime(required=True, readonly=True)
+
+    is_stale = Bool(title=_('Recipe is stale.'))
+
+
+class ISourcePackageRecipe(ISourcePackageRecipeData,
+    ISourcePackageRecipeEdit, ISourcePackageRecipeEditableAttributes,
+    ISourcePackageRecipeView):
+    """An ISourcePackageRecipe describes how to build a source package.
+
+    More precisely, it describes how to combine a number of branches into a
+    debianized source tree.
+    """
+    export_as_webservice_entry()
     base_branch = Reference(
         IBranch, title=_("The base branch used by this recipe."),
         required=True, readonly=True)
 
-    @operation_parameters(recipe_text=Text())
-    @export_write_operation()
-    def setRecipeText(recipe_text):
-        """Set the text of the recipe."""
-
-    recipe_text = exported(Text())
-
-    def isOverQuota(requester, distroseries):
-        """True if the recipe/requester/distroseries combo is >= quota.
-
-        :param requester: The Person requesting a build.
-        :param distroseries: The distroseries to build for.
-        """
-
-    @call_with(requester=REQUEST_USER)
-    @operation_parameters(
-        archive=Reference(schema=IArchive),
-        distroseries=Reference(schema=IDistroSeries),
-        pocket=Choice(vocabulary=PackagePublishingPocket,)
-        )
-    @export_write_operation()
-    def requestBuild(archive, distroseries, requester, pocket):
-        """Request that the recipe be built in to the specified archive.
-
-        :param archive: The IArchive which you want the build to end up in.
-        :param requester: the person requesting the build.
-        :param pocket: the pocket that should be targeted.
-        :raises: various specific upload errors if the requestor is not
-            able to upload to the archive.
-        """
-
-    def getBuilds(pending=False):
-        """Return a ResultSet of all the builds in the given state.
-
-        :param pending: If True, select all builds that are pending.  If
-            False, select all builds that are not pending.
-        """
-
-    def getLastBuild():
-        """Return the the most recent build of this recipe."""
-
-    def destroySelf():
-        """Remove this SourcePackageRecipe from the database.
-
-        This requires deleting any rows with non-nullable foreign key
-        references to this object.
-        """
-
 
 class ISourcePackageRecipeSource(Interface):
     """A utility of this interface can be used to create and access recipes.

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2010-09-03 11:54:23 +0000
+++ lib/lp/code/model/branch.py	2010-09-17 01:08:50 +0000
@@ -26,7 +26,6 @@
     And,
     Count,
     Desc,
-    Max,
     NamedFunc,
     Not,
     Or,
@@ -58,12 +57,8 @@
     ILaunchpadCelebrities,
     IPrivacy,
     )
+from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from canonical.launchpad.webapp import urlappend
-from canonical.launchpad.webapp.interfaces import (
-    IStoreSelector,
-    MAIN_STORE,
-    SLAVE_FLAVOR,
-    )
 from lp.app.errors import UserCannotUnsubscribePerson
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.code.bzr import (
@@ -804,6 +799,17 @@
             BranchRevision.branch == self,
             query).one()
 
+    def removeBranchRevisions(self, revision_ids):
+        """See `IBranch`."""
+        if isinstance(revision_ids, basestring):
+            revision_ids = [revision_ids]
+        IMasterStore(BranchRevision).find(
+            BranchRevision,
+            BranchRevision.branch == self,
+            BranchRevision.revision_id.is_in(
+                Select(Revision.id,
+                       Revision.revision_id.is_in(revision_ids)))).remove()
+
     def createBranchRevision(self, sequence, revision):
         """See `IBranch`."""
         branch_revision = BranchRevision(
@@ -921,13 +927,11 @@
         rows = rows.order_by(BranchRevision.sequence)
         ancestry = set()
         history = []
-        branch_revision_map = {}
         for branch_revision_id, sequence, revision_id in rows:
             ancestry.add(revision_id)
-            branch_revision_map[revision_id] = branch_revision_id
             if sequence is not None:
                 history.append(revision_id)
-        return ancestry, history, branch_revision_map
+        return ancestry, history
 
     def getPullURL(self):
         """See `IBranch`."""

=== modified file 'lib/lp/code/model/branchrevision.py'
--- lib/lp/code/model/branchrevision.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/branchrevision.py	2010-09-17 01:08:50 +0000
@@ -4,7 +4,9 @@
 # pylint: disable-msg=E0611,W0212
 
 __metaclass__ = type
-__all__ = ['BranchRevision', 'BranchRevisionSet']
+__all__ = [
+    'BranchRevision',
+    ]
 
 from storm.locals import (
     Int,
@@ -13,11 +15,7 @@
     )
 from zope.interface import implements
 
-from canonical.launchpad.interfaces.lpstorm import IMasterStore
-from lp.code.interfaces.branchrevision import (
-    IBranchRevision,
-    IBranchRevisionSet,
-    )
+from lp.code.interfaces.branchrevision import IBranchRevision
 
 
 class BranchRevision(Storm):
@@ -40,15 +38,3 @@
         self.branch = branch
         self.revision = revision
         self.sequence = sequence
-
-
-class BranchRevisionSet:
-    """See `IBranchRevisionSet`."""
-
-    implements(IBranchRevisionSet)
-
-    def delete(self, branch_revision_id):
-        """See `IBranchRevisionSet`."""
-        match = IMasterStore(BranchRevision).find(
-            BranchRevision, BranchRevision.id == branch_revision_id)
-        match.remove()

=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py	2010-09-01 03:25:36 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py	2010-09-17 01:08:50 +0000
@@ -11,7 +11,6 @@
     'SourcePackageRecipe',
     ]
 
-from bzrlib.plugins.builder.recipe import RecipeParser
 from lazr.delegates import delegates
 from storm.locals import (
     Bool,
@@ -139,33 +138,30 @@
             SourcePackageRecipeData,
             SourcePackageRecipeData.sourcepackage_recipe == self).one()
 
-    def _get_builder_recipe(self):
+    @property
+    def builder_recipe(self):
         """Accesses of the recipe go to the SourcePackageRecipeData."""
         return self._recipe_data.getRecipe()
 
-    def _set_builder_recipe(self, value):
-        """Setting of the recipe goes to the SourcePackageRecipeData."""
-        self._recipe_data.setRecipe(value)
-
-    builder_recipe = property(_get_builder_recipe, _set_builder_recipe)
-
     @property
     def base_branch(self):
         return self._recipe_data.base_branch
 
     def setRecipeText(self, recipe_text):
-        self.builder_recipe = RecipeParser(recipe_text).parse()
+        parsed = SourcePackageRecipeData.getParsedRecipe(recipe_text)
+        self._recipe_data.setRecipe(parsed)
 
     @property
     def recipe_text(self):
         return str(self.builder_recipe)
 
     @staticmethod
-    def new(registrant, owner, name, builder_recipe, description,
+    def new(registrant, owner, name, recipe, description,
             distroseries=None, daily_build_archive=None, build_daily=False):
         """See `ISourcePackageRecipeSource.new`."""
         store = IMasterStore(SourcePackageRecipe)
         sprecipe = SourcePackageRecipe()
+        builder_recipe = SourcePackageRecipeData.getParsedRecipe(recipe)
         SourcePackageRecipeData(builder_recipe, sprecipe)
         sprecipe.registrant = registrant
         sprecipe.owner = owner
@@ -201,6 +197,7 @@
         store = Store.of(self)
         self.distroseries.clear()
         self._recipe_data.instructions.find().remove()
+
         def destroyBuilds(pending):
             builds = self.getBuilds(pending=pending)
             for build in builds:

=== modified file 'lib/lp/code/model/sourcepackagerecipedata.py'
--- lib/lp/code/model/sourcepackagerecipedata.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/sourcepackagerecipedata.py	2010-09-17 01:08:50 +0000
@@ -18,6 +18,8 @@
     MergeInstruction,
     NestInstruction,
     RecipeBranch,
+    RecipeParser,
+    SAFE_INSTRUCTIONS,
     )
 from lazr.enum import (
     DBEnumeratedType,
@@ -39,7 +41,6 @@
 from canonical.database.enumcol import EnumCol
 from canonical.launchpad.interfaces.lpstorm import IStore
 from lp.code.errors import (
-    ForbiddenInstruction,
     NoSuchBranch,
     PrivateBranchRecipe,
     TooNewRecipeFormat,
@@ -151,6 +152,11 @@
         sourcepackage_recipe_build_id, 'SourcePackageRecipeBuild.id')
 
     @staticmethod
+    def getParsedRecipe(recipe_text):
+        parser = RecipeParser(recipe_text)
+        return parser.parse(permitted_instructions=SAFE_INSTRUCTIONS)
+
+    @staticmethod
     def findRecipes(branch):
         from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
         store = Store.of(branch)
@@ -179,10 +185,9 @@
             with.
         :return: an instance of SourcePackageRecipeData.
         """
-        from bzrlib.plugins.builder.recipe import RecipeParser
-        parser = RecipeParser(text)
-        return cls(parser.parse(),
-                   sourcepackage_recipe_build=sourcepackage_recipe_build)
+        parsed = cls.getParsedRecipe(text)
+        return cls(
+            parsed, sourcepackage_recipe_build=sourcepackage_recipe_build)
 
     def getRecipe(self):
         """The BaseRecipeBranch version of the recipe."""
@@ -213,9 +218,6 @@
         """
         r = {}
         for instruction in recipe_branch.child_branches:
-            if not (isinstance(instruction, MergeInstruction) or
-                    isinstance(instruction, NestInstruction)):
-                raise ForbiddenInstruction(str(instruction))
             db_branch = getUtility(IBranchLookup).getByUrl(
                 instruction.recipe_branch.url)
             if db_branch is None:

=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py	2010-09-03 00:25:53 +0000
+++ lib/lp/code/model/tests/test_branch.py	2010-09-17 01:08:50 +0000
@@ -29,6 +29,7 @@
 from canonical.database.constants import UTC_NOW
 from canonical.launchpad import _
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.interfaces.lpstorm import IStore
 from canonical.launchpad.webapp.interfaces import IOpenLaunchBag
 from canonical.testing import (
     DatabaseFunctionalLayer,
@@ -100,16 +101,16 @@
     ReclaimBranchSpaceJob,
     )
 from lp.code.model.branchmergeproposal import BranchMergeProposal
+from lp.code.model.branchrevision import BranchRevision
 from lp.code.model.codeimport import (
     CodeImport,
     CodeImportSet,
     )
 from lp.code.model.codereviewcomment import CodeReviewComment
+from lp.code.model.revision import Revision
 from lp.code.tests.helpers import add_revision_to_branch
 from lp.codehosting.bzrutils import UnsafeUrlSeen
-from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.registry.model.product import ProductSet
 from lp.registry.model.sourcepackage import SourcePackage
 from lp.services.osutils import override_environ
 from lp.testing import (
@@ -252,6 +253,59 @@
              branch.repository_format))
 
 
+class TestBranchRevisionMethods(TestCaseWithFactory):
+    """Test the branch methods for adding and removing branch revisions."""
+
+    layer = DatabaseFunctionalLayer
+
+    def _getBranchRevision(self, branch, rev_id):
+        """Get the branch revision for the specified branch and rev_id."""
+        resultset = IStore(BranchRevision).find(
+            BranchRevision,
+            BranchRevision.branch == branch,
+            BranchRevision.revision == Revision.id,
+            Revision.revision_id == rev_id)
+        return resultset.one()
+
+    def test_createBranchRevision(self):
+        # createBranchRevision adds the link for the revision to the branch.
+        branch = self.factory.makeBranch()
+        rev = self.factory.makeRevision()
+        # Nothing there to start with.
+        self.assertIs(None, self._getBranchRevision(branch, rev.revision_id))
+        branch.createBranchRevision(1, rev)
+        # Now there is one.
+        br = self._getBranchRevision(branch, rev.revision_id)
+        self.assertEqual(branch, br.branch)
+        self.assertEqual(rev, br.revision)
+
+    def test_removeBranchRevisions(self):
+        # removeBranchRevisions can remove a single linked revision.
+        branch = self.factory.makeBranch()
+        rev = self.factory.makeRevision()
+        branch.createBranchRevision(1, rev)
+        # Now remove the branch revision.
+        branch.removeBranchRevisions(rev.revision_id)
+        # Revision not there now.
+        self.assertIs(None, self._getBranchRevision(branch, rev.revision_id))
+
+    def test_removeBranchRevisions_multiple(self):
+        # removeBranchRevisions can remove multiple revision links at once.
+        branch = self.factory.makeBranch()
+        rev1 = self.factory.makeRevision()
+        rev2 = self.factory.makeRevision()
+        rev3 = self.factory.makeRevision()
+        branch.createBranchRevision(1, rev1)
+        branch.createBranchRevision(2, rev2)
+        branch.createBranchRevision(3, rev3)
+        # Now remove the branch revision.
+        branch.removeBranchRevisions(
+            [rev1.revision_id, rev2.revision_id, rev3.revision_id])
+        # No mainline revisions there now.
+        # The revision_history attribute is tested above.
+        self.assertEqual([], list(branch.revision_history))
+
+
 class TestBranchGetRevision(TestCaseWithFactory):
     """Make sure that `Branch.getBranchRevision` works as expected."""
 

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-09-01 03:25:36 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py	2010-09-17 01:08:50 +0000
@@ -14,7 +14,9 @@
 import textwrap
 import unittest
 
-from bzrlib.plugins.builder.recipe import RecipeParser
+from bzrlib.plugins.builder.recipe import (
+    ForbiddenInstructionError,
+)
 from pytz import UTC
 from storm.locals import Store
 import transaction
@@ -33,7 +35,6 @@
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.code.errors import (
     BuildAlreadyPending,
-    ForbiddenInstruction,
     PrivateBranchRecipe,
     TooManyBuilds,
     TooNewRecipeFormat,
@@ -84,18 +85,6 @@
         recipe = self.factory.makeSourcePackageRecipe()
         verifyObject(ISourcePackageRecipe, recipe)
 
-    def makeSourcePackageRecipeFromBuilderRecipe(self, builder_recipe):
-        """Make a SourcePackageRecipe from a recipe with arbitrary other data.
-        """
-        registrant = self.factory.makePerson()
-        owner = self.factory.makeTeam(owner=registrant)
-        distroseries = self.factory.makeDistroSeries()
-        name = self.factory.getUniqueString(u'recipe-name')
-        description = self.factory.getUniqueString(u'recipe-description')
-        return getUtility(ISourcePackageRecipeSource).new(
-            registrant=registrant, owner=owner, distroseries=[distroseries],
-            name=name, description=description, builder_recipe=builder_recipe)
-
     def makeRecipeComponents(self, branches=()):
         """Return a dict of values that can be used to make a recipe.
 
@@ -110,7 +99,7 @@
             distroseries = [self.factory.makeDistroSeries()],
             name = self.factory.getUniqueString(u'recipe-name'),
             description = self.factory.getUniqueString(u'recipe-description'),
-            builder_recipe = self.factory.makeRecipe(*branches))
+            recipe = self.factory.makeRecipeText(*branches))
 
     def test_creation(self):
         # The metadata supplied when a SourcePackageRecipe is created is
@@ -174,17 +163,15 @@
 
     def test_recipe_implements_interface(self):
         # SourcePackageRecipe objects implement ISourcePackageRecipe.
-        recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            self.factory.makeRecipe())
+        recipe = self.factory.makeSourcePackageRecipe()
         transaction.commit()
-        self.assertProvides(recipe, ISourcePackageRecipe)
+        with person_logged_in(recipe.owner):
+            self.assertProvides(recipe, ISourcePackageRecipe)
 
     def test_base_branch(self):
         # When a recipe is created, we can access its base branch.
         branch = self.factory.makeAnyBranch()
-        builder_recipe = self.factory.makeRecipe(branch)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe)
+        sp_recipe = self.factory.makeSourcePackageRecipe(branches=[branch])
         transaction.commit()
         self.assertEquals(branch, sp_recipe.base_branch)
 
@@ -192,9 +179,8 @@
         # When a recipe is created, we can query it for links to the branch
         # it references.
         branch = self.factory.makeAnyBranch()
-        builder_recipe = self.factory.makeRecipe(branch)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe)
+        sp_recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch])
         transaction.commit()
         self.assertEquals([branch], list(sp_recipe.getReferencedBranches()))
 
@@ -203,9 +189,8 @@
         # returns all of them.
         branch1 = self.factory.makeAnyBranch()
         branch2 = self.factory.makeAnyBranch()
-        builder_recipe = self.factory.makeRecipe(branch1, branch2)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe)
+        sp_recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch1, branch2])
         transaction.commit()
         self.assertEquals(
             sorted([branch1, branch2]),
@@ -214,27 +199,23 @@
     def test_random_user_cant_edit(self):
         # An arbitrary user can't set attributes.
         branch1 = self.factory.makeAnyBranch()
-        builder_recipe1 = self.factory.makeRecipe(branch1)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe1)
-        branch2 = self.factory.makeAnyBranch()
-        builder_recipe2 = self.factory.makeRecipe(branch2)
+        recipe_1 = self.factory.makeRecipeText(branch1)
+        sp_recipe = self.factory.makeSourcePackageRecipe(
+            recipe=recipe_1)
         login_person(self.factory.makePerson())
         self.assertRaises(
-            Unauthorized, setattr, sp_recipe, 'builder_recipe',
-            builder_recipe2)
+            Unauthorized, getattr, sp_recipe, 'setRecipeText')
 
     def test_set_recipe_text_resets_branch_references(self):
         # When the recipe_text is replaced, getReferencedBranches returns
         # (only) the branches referenced by the new recipe.
         branch1 = self.factory.makeAnyBranch()
-        builder_recipe1 = self.factory.makeRecipe(branch1)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe1)
+        sp_recipe = self.factory.makeSourcePackageRecipe(
+            branches=[branch1])
         branch2 = self.factory.makeAnyBranch()
-        builder_recipe2 = self.factory.makeRecipe(branch2)
-        login_person(sp_recipe.owner.teamowner)
-        sp_recipe.builder_recipe = builder_recipe2
+        new_recipe = self.factory.makeRecipeText(branch2)
+        with person_logged_in(sp_recipe.owner):
+            sp_recipe.setRecipeText(new_recipe)
         self.assertEquals([branch2], list(sp_recipe.getReferencedBranches()))
 
     def test_rejects_run_command(self):
@@ -243,36 +224,32 @@
         %(base)s
         run touch test
         ''' % dict(base=self.factory.makeAnyBranch().bzr_identity)
-        parser = RecipeParser(textwrap.dedent(recipe_text))
-        builder_recipe = parser.parse()
+        recipe_text = textwrap.dedent(recipe_text)
         self.assertRaises(
-            ForbiddenInstruction,
-            self.makeSourcePackageRecipeFromBuilderRecipe, builder_recipe)
+            ForbiddenInstructionError, self.factory.makeSourcePackageRecipe,
+            recipe=recipe_text)
 
     def test_run_rejected_without_mangling_recipe(self):
-        branch1 = self.factory.makeAnyBranch()
-        builder_recipe1 = self.factory.makeRecipe(branch1)
-        sp_recipe = self.makeSourcePackageRecipeFromBuilderRecipe(
-            builder_recipe1)
+        sp_recipe = self.factory.makeSourcePackageRecipe()
+        old_branches = list(sp_recipe.getReferencedBranches())
         recipe_text = '''\
         # bzr-builder format 0.2 deb-version 0.1-{revno}
         %(base)s
         run touch test
         ''' % dict(base=self.factory.makeAnyBranch().bzr_identity)
-        parser = RecipeParser(textwrap.dedent(recipe_text))
-        builder_recipe2 = parser.parse()
-        login_person(sp_recipe.owner.teamowner)
-        self.assertRaises(
-            ForbiddenInstruction, setattr, sp_recipe, 'builder_recipe',
-            builder_recipe2)
-        self.assertEquals([branch1], list(sp_recipe.getReferencedBranches()))
+        recipe_text = textwrap.dedent(recipe_text)
+        with person_logged_in(sp_recipe.owner):
+            self.assertRaises(
+                ForbiddenInstructionError, sp_recipe.setRecipeText, recipe_text)
+        self.assertEquals(
+            old_branches, list(sp_recipe.getReferencedBranches()))
 
     def test_reject_newer_formats(self):
         builder_recipe = self.factory.makeRecipe()
         builder_recipe.format = 0.3
         self.assertRaises(
             TooNewRecipeFormat,
-            self.makeSourcePackageRecipeFromBuilderRecipe, builder_recipe)
+            self.factory.makeSourcePackageRecipe, recipe=str(builder_recipe))
 
     def test_requestBuild(self):
         recipe = self.factory.makeSourcePackageRecipe()
@@ -484,7 +461,8 @@
         self.factory.makeSourcePackageRecipeBuildJob(
             recipe_build=past_build)
         removeSecurityProxy(past_build).datebuilt = datetime.now(UTC)
-        recipe.destroySelf()
+        with person_logged_in(recipe.owner):
+            recipe.destroySelf()
         # Show no database constraints were violated
         Store.of(recipe).flush()
 
@@ -501,6 +479,7 @@
             SourcePackageRecipe.findStaleDailyBuilds())
 
     def test_getMedianBuildDuration(self):
+
         def set_duration(build, minutes):
             duration = timedelta(minutes=minutes)
             build = removeSecurityProxy(build)
@@ -540,7 +519,7 @@
             }
 
     def get_recipe(self, recipe_text):
-        builder_recipe = RecipeParser(textwrap.dedent(recipe_text)).parse()
+        recipe_text = textwrap.dedent(recipe_text)
         registrant = self.factory.makePerson()
         owner = self.factory.makeTeam(owner=registrant)
         distroseries = self.factory.makeDistroSeries()
@@ -548,7 +527,7 @@
         description = self.factory.getUniqueString(u'recipe-description')
         recipe = getUtility(ISourcePackageRecipeSource).new(
             registrant=registrant, owner=owner, distroseries=[distroseries],
-            name=name, description=description, builder_recipe=builder_recipe)
+            name=name, description=description, recipe=recipe_text)
         transaction.commit()
         return recipe.builder_recipe
 
@@ -722,8 +701,8 @@
         return MINIMAL_RECIPE_TEXT % branch.bzr_identity
 
     def makeRecipe(self, user=None, owner=None, recipe_text=None):
-        # rockstar 21 Jul 2010 - This function does more commits than I'd like,
-        # but it's the result of the fact that the webservice runs in a
+        # rockstar 21 Jul 2010 - This function does more commits than I'd
+        # like, but it's the result of the fact that the webservice runs in a
         # separate thread so doesn't get the database updates without those
         # commits.
         if user is None:

=== modified file 'lib/lp/code/stories/branches/xx-bazaar-home.txt'
--- lib/lp/code/stories/branches/xx-bazaar-home.txt	2010-08-27 04:53:51 +0000
+++ lib/lp/code/stories/branches/xx-bazaar-home.txt	2010-09-17 01:08:50 +0000
@@ -30,7 +30,7 @@
 
     >>> preview = find_tag_by_id(browser.contents, 'project-cloud-preview')
     >>> print extract_text(preview)
-    Projects with active branches
+    Most active projects in the last month
     see all projects&#8230;
 
     >>> print preview.fetch('a')[-1]['href']

=== modified file 'lib/lp/code/templates/bazaar-index.pt'
--- lib/lp/code/templates/bazaar-index.pt	2010-07-12 14:48:32 +0000
+++ lib/lp/code/templates/bazaar-index.pt	2010-09-17 01:08:50 +0000
@@ -54,7 +54,7 @@
       </p>
 
       <div id="project-cloud-preview" tal:content="cache:public,6 hours">
-        <h2>Projects with active branches</h2>
+        <h2>Most active projects in the last month</h2>
         <a tal:repeat="product view/short_product_tag_cloud"
            tal:attributes="href product/url;
                            class product/html_class;

=== modified file 'lib/lp/codehosting/scanner/bzrsync.py'
--- lib/lp/codehosting/scanner/bzrsync.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/scanner/bzrsync.py	2010-09-17 01:08:50 +0000
@@ -21,8 +21,9 @@
 from zope.component import getUtility
 from zope.event import notify
 
+from canonical.config import config
+
 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
-from lp.code.interfaces.branchrevision import IBranchRevisionSet
 from lp.code.interfaces.revision import IRevisionSet
 from lp.codehosting import iter_list_chunks
 from lp.codehosting.scanner import events
@@ -82,13 +83,11 @@
         # written to by the branch-scanner, so they are not subject to
         # write-lock contention. Update them all in a single transaction to
         # improve the performance and allow garbage collection in the future.
-        db_ancestry, db_history, db_branch_revision_map = (
-            self.retrieveDatabaseAncestry())
+        db_ancestry, db_history = self.retrieveDatabaseAncestry()
 
         (added_ancestry, branchrevisions_to_delete,
             revids_to_insert) = self.planDatabaseChanges(
-            bzr_branch, bzr_ancestry, bzr_history, db_ancestry, db_history,
-            db_branch_revision_map)
+            bzr_branch, bzr_ancestry, bzr_history, db_ancestry, db_history)
         added_ancestry.difference_update(
             getUtility(IRevisionSet).onlyPresent(added_ancestry))
         self.logger.info("Adding %s new revisions.", len(added_ancestry))
@@ -129,9 +128,8 @@
     def retrieveDatabaseAncestry(self):
         """Efficiently retrieve ancestry from the database."""
         self.logger.info("Retrieving ancestry from database.")
-        db_ancestry, db_history, db_branch_revision_map = (
-            self.db_branch.getScannerData())
-        return db_ancestry, db_history, db_branch_revision_map
+        db_ancestry, db_history = self.db_branch.getScannerData()
+        return db_ancestry, db_history
 
     def retrieveBranchDetails(self, bzr_branch):
         """Retrieve ancestry from the the bzr branch on disk."""
@@ -147,7 +145,7 @@
         return bzr_ancestry, bzr_history
 
     def planDatabaseChanges(self, bzr_branch, bzr_ancestry, bzr_history,
-                            db_ancestry, db_history, db_branch_revision_map):
+                            db_ancestry, db_history):
         """Plan database changes to synchronize with bzrlib data.
 
         Use the data retrieved by `retrieveDatabaseAncestry` and
@@ -190,9 +188,8 @@
 
         # We must delete BranchRevision rows for all revisions which where
         # removed from the ancestry or whose sequence value has changed.
-        branchrevisions_to_delete = set(
-            db_branch_revision_map[revid]
-            for revid in removed_merged.union(removed_history))
+        branchrevisions_to_delete = list(
+            removed_merged.union(removed_history))
 
         # We must insert BranchRevision rows for all revisions which were
         # added to the ancestry or whose sequence value has changed.
@@ -244,13 +241,19 @@
         for revision_id in revision_subset.difference(set(bzr_history)):
             yield revision_id, None
 
-    def deleteBranchRevisions(self, branchrevisions_to_delete):
+    def deleteBranchRevisions(self, revision_ids_to_delete):
         """Delete a batch of BranchRevision rows."""
         self.logger.info("Deleting %d branchrevision records.",
-            len(branchrevisions_to_delete))
-        branch_revision_set = getUtility(IBranchRevisionSet)
-        for branchrevision in sorted(branchrevisions_to_delete):
-            branch_revision_set.delete(branchrevision)
+            len(revision_ids_to_delete))
+        # Use a config value to work out how many to delete at a time.
+        # Deleting more than one at a time is significantly more efficient
+        # than doing one at a time, but the actual optimal count is a bit up
+        # in the air.
+        batch_size = config.branchscanner.branch_revision_delete_count
+        while revision_ids_to_delete:
+            batch = revision_ids_to_delete[:batch_size]
+            revision_ids_to_delete[:batch_size] = []
+            self.db_branch.removeBranchRevisions(batch)
 
     def insertBranchRevisions(self, bzr_branch, revids_to_insert):
         """Insert a batch of BranchRevision rows."""

=== modified file 'lib/lp/codehosting/scanner/tests/test_buglinks.py'
--- lib/lp/codehosting/scanner/tests/test_buglinks.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/scanner/tests/test_buglinks.py	2010-09-17 01:08:50 +0000
@@ -12,6 +12,7 @@
 from bzrlib.revision import Revision
 from zope.component import getUtility
 from zope.event import notify
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.config import config
 from canonical.launchpad.interfaces import (
@@ -120,8 +121,16 @@
     def makeFixtures(self):
         super(TestBugLinking, self).makeFixtures()
         self.bug1 = self.factory.makeBug()
+        sp = self.factory.makeSourcePackage()
+        self.bug1.addTask(self.bug1.owner, sp)
+        dsp = self.factory.makeDistributionSourcePackage()
+        self.bug1.addTask(self.bug1.owner, dsp)
+        distro = self.factory.makeDistribution()
+        self.bug1.addTask(self.bug1.owner, distro)
         self.bug2 = self.factory.makeBug()
         self.new_db_branch = self.factory.makeAnyBranch()
+        removeSecurityProxy(distro).max_bug_heat = 0;
+        removeSecurityProxy(dsp).max_bug_heat = 0;
         self.layer.txn.commit()
 
     def getBugURL(self, bug):

=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
--- lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-09-17 01:08:50 +0000
@@ -20,6 +20,7 @@
 from bzrlib.tests import TestCaseWithTransport
 from bzrlib.uncommit import uncommit
 import pytz
+from storm.locals import Store
 import transaction
 from twisted.python.util import mergeFunctionMetadata
 from zope.component import getUtility
@@ -79,6 +80,8 @@
         self.lp_db_user = config.launchpad.dbuser
         self.makeFixtures()
         LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
+        # Catch both constraints and permissions for the db user.
+        self.addCleanup(Store.of(self.db_branch).flush)
 
     def tearDown(self):
         super(BzrSyncTestCase, self).tearDown()
@@ -490,18 +493,14 @@
         expected_history = [branch_revision.revision.revision_id
             for branch_revision in sampledata
             if branch_revision.sequence is not None]
-        expected_mapping = dict(
-            (branch_revision.revision.revision_id, branch_revision.id)
-            for branch_revision in sampledata)
 
         self.create_branch_and_tree(db_branch=branch)
 
         bzrsync = self.makeBzrSync(branch)
-        db_ancestry, db_history, db_branch_revision_map = (
+        db_ancestry, db_history = (
             bzrsync.retrieveDatabaseAncestry())
         self.assertEqual(expected_ancestry, set(db_ancestry))
         self.assertEqual(expected_history, list(db_history))
-        self.assertEqual(expected_mapping, db_branch_revision_map)
 
 
 class TestBzrSyncOneRevision(BzrSyncTestCase):

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-08-27 04:33:52 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-09-17 01:08:50 +0000
@@ -143,6 +143,12 @@
         for="lp.registry.interfaces.distroseries.IDistroSeries"
         class="canonical.launchpad.browser.AskAQuestionButtonView"
         permission="zope.Public"/>
+    <browser:page
+        name="+localpackagediffs"
+        for="lp.registry.interfaces.distroseries.IDistroSeries"
+        class="lp.registry.browser.distroseries.DistroSeriesLocalDifferences"
+        template="../templates/distroseries-localdifferences.pt"
+        permission="zope.Public"/>
     <browser:menus
         classes="
             DistroSeriesFacets

=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py	2010-09-03 03:12:39 +0000
+++ lib/lp/registry/browser/distribution.py	2010-09-17 01:08:50 +0000
@@ -74,6 +74,7 @@
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.launchpad.webapp.interfaces import ILaunchBag
 from canonical.widgets.image import ImageChangeWidget
+from lp.app.enums import service_uses_launchpad
 from lp.answers.browser.faqtarget import FAQTargetNavigationMixin
 from lp.answers.browser.questiontarget import (
     QuestionTargetFacetMixin,
@@ -122,10 +123,10 @@
         """Return a string of LP apps (comma-separated) this distro uses."""
         uses = []
         href_template = """<a href="%s">%s</a>"""
-        if self.context.official_answers:
+        if service_uses_launchpad(self.context.answers_usage):
             url = canonical_url(self.context, rootsite='answers')
             uses.append(href_template % (url, 'Answers'))
-        if self.context.official_blueprints:
+        if service_uses_launchpad(self.context.blueprints_usage):
             url = canonical_url(self.context, rootsite='blueprints')
             uses.append(href_template % (url, 'Blueprints'))
         if self.context.official_malone:
@@ -135,7 +136,7 @@
             if self.context.codehosting_usage == ServiceUsage.LAUNCHPAD:
                 url = canonical_url(self.context, rootsite='code')
                 uses.append(href_template % (url, 'Branches'))
-        if self.context.official_rosetta:
+        if service_uses_launchpad(self.context.translations_usage):
             url = canonical_url(self.context, rootsite='translations')
             uses.append(href_template % (url, 'Translations'))
 

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2010-09-03 15:02:39 +0000
+++ lib/lp/registry/browser/distroseries.py	2010-09-17 01:08:50 +0000
@@ -11,6 +11,7 @@
     'DistroSeriesBreadcrumb',
     'DistroSeriesEditView',
     'DistroSeriesFacets',
+    'DistroSeriesLocalDifferences',
     'DistroSeriesPackageSearchView',
     'DistroSeriesPackagesView',
     'DistroSeriesNavigation',
@@ -70,7 +71,10 @@
     StructuralSubscriptionTargetTraversalMixin,
     )
 from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.distroseriesdifference import (
+    IDistroSeriesDifferenceSource)
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services.features.flags import FeatureController
 from lp.services.propertycache import cachedproperty
 from lp.services.worlddata.interfaces.country import ICountry
 from lp.services.worlddata.interfaces.language import ILanguageSet
@@ -525,3 +529,36 @@
         navigator = BatchNavigator(packages, self.request, size=20)
         navigator.setHeadings('package', 'packages')
         return navigator
+
+
+class DistroSeriesLocalDifferences(LaunchpadView):
+    """Present differences between a derived series and its parent."""
+
+    page_title = 'Local package differences'
+
+    def initialize(self):
+        """Redirect to the derived series if the feature is not enabled."""
+        def in_scope(value):
+            return True
+
+        feature_controller = FeatureController(in_scope)
+        if feature_controller.getFlag('soyuz.derived-series-ui.enabled') != 'on':
+            self.request.response.redirect(canonical_url(self.context))
+            return
+        super(DistroSeriesLocalDifferences, self).initialize()
+
+    @property
+    def label(self):
+        return (
+            "Source package differences between '%s' and "
+            "parent series '%s'" % (
+                self.context.displayname,
+                self.context.parent_series.displayname,
+                ))
+
+    @cachedproperty
+    def cached_differences(self):
+        """Return a batch navigator of potentially filtered results."""
+        differences = getUtility(IDistroSeriesDifferenceSource).getForDistroSeries(
+            self.context)
+        return BatchNavigator(differences, self.request)

=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2010-09-13 13:38:01 +0000
+++ lib/lp/registry/browser/person.py	2010-09-17 01:08:50 +0000
@@ -208,9 +208,7 @@
     DateTimeFormatterAPI,
     PersonFormatterAPI,
     )
-from canonical.lazr.utils import (
-    smartquote,
-    )
+from canonical.lazr.utils import smartquote
 from canonical.widgets import (
     LaunchpadDropdownWidget,
     LaunchpadRadioWidget,
@@ -4443,7 +4441,7 @@
                 verb = 'have been'
                 team_string= (
                     ', '.join(team_names[:-1]) + ' and ' + team_names[-1])
-            full_message += '%s %s %s ' % (team_string, verb, message)
+            full_message += '%s %s %s' % (team_string, verb, message)
         self.request.response.addInfoNotification(full_message)
 
 

=== modified file 'lib/lp/registry/browser/pillar.py'
--- lib/lp/registry/browser/pillar.py	2010-09-03 03:12:39 +0000
+++ lib/lp/registry/browser/pillar.py	2010-09-17 01:08:50 +0000
@@ -30,7 +30,10 @@
     nearest,
     )
 from canonical.launchpad.webapp.tales import MenuAPI
-from lp.app.enums import ServiceUsage
+from lp.app.enums import (
+    ServiceUsage,
+    service_uses_launchpad,
+    )
 from lp.app.interfaces.launchpad import IServiceUsage
 from lp.registry.browser.structuralsubscription import (
     StructuralSubscriptionMenuMixin,
@@ -67,12 +70,12 @@
     def ask_question(self):
         return Link(
             '+addquestion', 'Ask a question', site='answers', icon='answers',
-            enabled=self.pillar.official_answers)
+            enabled=service_uses_launchpad(self.pillar.answers_usage))
 
     def help_translate(self):
         return Link(
             '', 'Help translate', site='translations', icon='translations',
-            enabled=self.pillar.official_rosetta)
+            enabled=service_uses_launchpad(self.pillar.translations_usage))
 
     def submit_code(self):
         if self.pillar.codehosting_usage in [
@@ -88,8 +91,11 @@
 
     def register_blueprint(self):
         return Link(
-            '+addspec', 'Register a blueprint', site='blueprints',
-            icon='blueprints', enabled=self.pillar.official_blueprints)
+            '+addspec',
+            'Register a blueprint',
+            site='blueprints',
+            icon='blueprints',
+            enabled=service_uses_launchpad(self.pillar.blueprints_usage))
 
 
 class PillarView(LaunchpadView):
@@ -102,9 +108,9 @@
     def __init__(self, context, request):
         super(PillarView, self).__init__(context, request)
         self.official_malone = False
-        self.official_answers = False
-        self.official_blueprints = False
-        self.official_rosetta = False
+        self.answers_usage = ServiceUsage.UNKNOWN
+        self.blueprints_usage = ServiceUsage.UNKNOWN
+        self.translations_usage = ServiceUsage.UNKNOWN
         self.codehosting_usage = ServiceUsage.UNKNOWN
         pillar = nearest(self.context, IPillar)
         if IProjectGroup.providedBy(pillar):
@@ -116,12 +122,12 @@
         else:
             self._set_official_launchpad(pillar)
             if IDistroSeries.providedBy(self.context):
-                self.official_answers = False
                 distribution = self.context.distribution
                 self.codehosting_usage = distribution.codehosting_usage
+                self.answers_usage = ServiceUsage.NOT_APPLICABLE
             elif IDistributionSourcePackage.providedBy(self.context):
-                self.official_blueprints = False
-                self.official_rosetta = False
+                self.blueprints_usage = ServiceUsage.UNKNOWN
+                self.translations_usage = ServiceUsage.UNKNOWN
             else:
                 # The context is used by all apps.
                 pass
@@ -132,21 +138,23 @@
         # times to build the complete set of official applications.
         if pillar.official_malone:
             self.official_malone = True
-        if pillar.official_answers:
-            self.official_answers = True
-        if pillar.official_blueprints:
-            self.official_blueprints = True
-        if pillar.official_rosetta:
-            self.official_rosetta = True
-        self.codehosting_usage = IServiceUsage(pillar).codehosting_usage
+        if service_uses_launchpad(IServiceUsage(pillar).answers_usage):
+            self.answers_usage = ServiceUsage.LAUNCHPAD
+        if service_uses_launchpad(IServiceUsage(pillar).blueprints_usage):
+            self.blueprints_usage = ServiceUsage.LAUNCHPAD
+        if service_uses_launchpad(pillar.translations_usage):
+            self.translations_usage = ServiceUsage.LAUNCHPAD
+        if service_uses_launchpad(IServiceUsage(pillar).codehosting_usage):
+            self.codehosting_usage = ServiceUsage.LAUNCHPAD
 
     @property
     def has_involvement(self):
         """This `IPillar` uses Launchpad."""
-        return (
-            self.official_malone or self.official_answers
-            or self.official_blueprints or self.official_rosetta
-            or self.codehosting_usage == ServiceUsage.LAUNCHPAD)
+        return (self.official_malone
+            or service_uses_launchpad(self.answers_usage)
+            or service_uses_launchpad(self.blueprints_usage)
+            or service_uses_launchpad(self.translations_usage)
+            or service_uses_launchpad(self.codehosting_usage))
 
     @property
     def enabled_links(self):

=== modified file 'lib/lp/registry/browser/productseries.py'
--- lib/lp/registry/browser/productseries.py	2010-09-03 15:02:39 +0000
+++ lib/lp/registry/browser/productseries.py	2010-09-17 01:08:50 +0000
@@ -254,11 +254,11 @@
 
     def __init__(self, context, request):
         super(ProductSeriesInvolvementView, self).__init__(context, request)
+        self.answers_usage = ServiceUsage.NOT_APPLICABLE
         if self.context.branch is not None:
             self.codehosting_usage = ServiceUsage.LAUNCHPAD
         else:
             self.codehosting_usage = ServiceUsage.UNKNOWN
-        self.official_answers = False
 
     @property
     def configuration_links(self):

=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py	2010-09-03 03:12:39 +0000
+++ lib/lp/registry/browser/team.py	2010-09-17 01:08:50 +0000
@@ -89,7 +89,10 @@
     TeamContactMethod,
     TeamSubscriptionPolicy,
     )
-from lp.registry.interfaces.teammembership import TeamMembershipStatus
+from lp.registry.interfaces.teammembership import (
+    CyclicalTeamMembershipError,
+    TeamMembershipStatus,
+    )
 from lp.services.fields import PublicPersonChoice
 from lp.services.propertycache import cachedproperty
 
@@ -804,21 +807,29 @@
         assert(self.mailing_list is not None), (
             'No mailing list: %s' % self.context.name)
 
-    @property
+    @cachedproperty
     def hold_count(self):
         """The number of message being held for moderator approval.
 
         :return: Number of message being held for moderator approval.
         """
-        return self.mailing_list.getReviewableMessages().count()
+        ## return self.mailing_list.getReviewableMessages().count()
+        # This looks like it would be more efficient, but it raises
+        # LocationError.
+        return self.held_messages.currentBatch().listlength
 
-    @property
+    @cachedproperty
     def held_messages(self):
         """All the messages being held for moderator approval.
 
         :return: Sequence of held messages.
         """
-        return self.mailing_list.getReviewableMessages()
+        results = self.mailing_list.getReviewableMessages()
+        navigator = BatchNavigator(results, self.request)
+        # Subclasses often set the singular and plural headings,
+        # but we can use the generic class too.
+        navigator.setHeadings('message', 'messages')
+        return navigator
 
     @action('Moderate', name='moderate')
     def moderate_action(self, action, data):
@@ -827,7 +838,7 @@
         # won't be in data.  Instead, get it out of the request.
         reviewable = self.hold_count
         disposed_count = 0
-        for message in self.held_messages:
+        for message in self.held_messages.currentBatch():
             action_name = self.request.form_ng.getOne(
                 'field.' + quote(message.message_id))
             # This essentially acts like a switch statement or if/elifs.  It
@@ -934,31 +945,60 @@
     @action('Save changes', name='save')
     def action_save(self, action, data):
         expires = self.context.defaultexpirationdate
-        for person in self.context.proposedmembers:
+        statuses = dict(
+            approve=TeamMembershipStatus.APPROVED,
+            decline=TeamMembershipStatus.DECLINED,
+            )
+        target_team = self.context
+        failed_joins = []
+        for person in target_team.proposedmembers:
             action = self.request.form.get('action_%d' % person.id)
-            if action == "approve":
-                status = TeamMembershipStatus.APPROVED
-            elif action == "decline":
-                status = TeamMembershipStatus.DECLINED
-            else:
+            status = statuses.get(action)
+            if status is None:
                 # The action is "hold" or no action was specified for this
                 # person, which could happen if the set of proposed members
                 # changed while the form was being processed.
                 continue
-
-            self.context.setMembershipData(
-                person, status, reviewer=self.user, expires=expires,
-                comment=self.request.form.get('comment'))
+            try:
+                target_team.setMembershipData(
+                    person, status, reviewer=self.user, expires=expires,
+                    comment=self.request.form.get('comment'))
+            except CyclicalTeamMembershipError:
+                failed_joins.append(person)
+
+        if len(failed_joins) > 0:
+            failed_names = [person.displayname for person in failed_joins]
+            failed_list = ", ".join(failed_names)
+
+            mapping=dict(
+                this_team=target_team.displayname,
+                failed_list=failed_list)
+
+            if len(failed_joins) == 1:
+                self.request.response.addInfoNotification(
+                    _('${this_team} is a member of the following team, so it '
+                      'could not be accepted:  '
+                      '${failed_list}.  You need to "Decline" that team.',
+                      mapping=mapping))
+            else:
+                self.request.response.addInfoNotification(
+                    _('${this_team} is a member of the following teams, so '
+                      'they could not be accepted:  '
+                      '${failed_list}.  You need to "Decline" those teams.',
+                      mapping=mapping))
+            self.next_url = ''
+        else:
+            self.next_url = self._next_url
 
     @property
     def page_title(self):
         return 'Proposed members of %s' % self.context.displayname
 
     @property
-    def next_url(self):
+    def _next_url(self):
         return '%s/+members' % canonical_url(self.context)
 
-    cancel_url = next_url
+    cancel_url = _next_url
 
 
 class TeamBrandingView(BrandingChangeView):

=== modified file 'lib/lp/registry/browser/tests/distribution-views.txt'
--- lib/lp/registry/browser/tests/distribution-views.txt	2010-08-30 18:26:19 +0000
+++ lib/lp/registry/browser/tests/distribution-views.txt	2010-09-17 01:08:50 +0000
@@ -274,11 +274,12 @@
 If the distribution officially uses the application, its portlet does appear.
 
     >>> from canonical.testing.layers import MemcachedLayer
+    >>> from lp.app.enums import ServiceUsage
 
     # When the cache regenerated, all users will see the change.
     >>> MemcachedLayer.purge()
-    >>> distribution.official_answers = True
-    >>> distribution.official_blueprints = True
+    >>> distribution.answers_usage = ServiceUsage.LAUNCHPAD
+    >>> distribution.blueprints_usage = ServiceUsage.LAUNCHPAD
     >>> distribution.official_malone = True
 
     >>> view = create_view(distribution, name='+index', principal=owner)

=== modified file 'lib/lp/registry/browser/tests/pillar-views.txt'
--- lib/lp/registry/browser/tests/pillar-views.txt	2010-08-27 17:45:57 +0000
+++ lib/lp/registry/browser/tests/pillar-views.txt	2010-09-17 01:08:50 +0000
@@ -24,7 +24,8 @@
 
 Pillars that do use launchpad applications have an involvement menu.
 
-    >>> distribution.official_answers = True
+    >>> from lp.app.enums import ServiceUsage
+    >>> distribution.answers_usage = ServiceUsage.LAUNCHPAD
     >>> distribution.official_malone = True
     >>> view = create_view(
     ...     distribution, '+get-involved', principal=distribution.owner)
@@ -33,14 +34,14 @@
 
     >>> view.official_malone
     True
-    >>> view.official_answers
-    True
-    >>> view.official_rosetta
-    False
-    >>> view.official_blueprints
-    False
-    >>> view.codehosting_usage.name
-    'NOT_APPLICABLE'
+    >>> print view.answers_usage.name
+    LAUNCHPAD
+    >>> view.translations_usage.name
+    'UNKNOWN'
+    >>> print view.blueprints_usage.name
+    UNKNOWN
+    >>> print view.codehosting_usage.name
+    UNKNOWN
 
 The view provides a list of enabled links that is rendered by the template.
 
@@ -65,10 +66,10 @@
 
     >>> product = factory.makeProduct(name='bread')
     >>> login_person(product.owner)
-    >>> product.official_blueprints = True
+    >>> product.blueprints_usage = ServiceUsage.LAUNCHPAD
     >>> view = create_view(product, '+get-involved')
-    >>> view.official_blueprints
-    True
+    >>> print view.blueprints_usage.name
+    LAUNCHPAD
     >>> for link in view.enabled_links:
     ...     print link.name
     register_blueprint
@@ -114,7 +115,6 @@
 
 Changing the product's usage is reflected in the view properties.
 
-    >>> from lp.app.enums import ServiceUsage
     >>> product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> view = create_view(product, '+get-involved')
     >>> for key in sorted(view.configuration_states.keys()):
@@ -189,8 +189,8 @@
     >>> product.project = project_group
 
     >>> view = create_view(project_group, '+get-involved')
-    >>> view.official_blueprints
-    True
+    >>> print view.blueprints_usage.name
+    LAUNCHPAD
 
 The offical_codehosting for a project is based on whether the project's
 development focus series has a branch.
@@ -200,23 +200,23 @@
     >>> product.official_codehosting
     False
     >>> view = create_view(product, '+get-involved')
-    >>> view.codehosting_usage.name
-    'UNKNOWN'
+    >>> print view.codehosting_usage.name
+    UNKNOWN
 
     >>> product.development_focus.branch = factory.makeBranch(
     ...     product=product)
     >>> product.official_codehosting
     True
     >>> view = create_view(product, '+get-involved')
-    >>> view.codehosting_usage.name
-    'LAUNCHPAD' 
+    >>> print view.codehosting_usage.name
+    LAUNCHPAD
 
 Project groups cannot make links to register a branch, so
 official_codehosting is always false.
 
     >>> view = create_view(project_group, '+get-involved')
-    >>> view.codehosting_usage.name
-    'NOT_APPLICABLE'
+    >>> print view.codehosting_usage.name
+    NOT_APPLICABLE
 
 DistroSeries can use this view. The distribution is used to set the links.
 
@@ -230,9 +230,10 @@
 set the links.  Despite the fact that the distribution uses blueprints,
 and translations those links are not enabled for DistributionSourcePackages.
 
+    >>> from lp.app.enums import ServiceUsage
     >>> login_person(distribution.owner)
-    >>> distribution.official_blueprints = True
-    >>> distribution.official_rosetta = True
+    >>> distribution.blueprints_usage = ServiceUsage.LAUNCHPAD
+    >>> distribution.translations_usage = ServiceUsage.LAUNCHPAD
     >>> package = factory.makeDistributionSourcePackage(
     ...     sourcepackagename="box",
     ...     distribution=distribution)

=== modified file 'lib/lp/registry/browser/tests/product-views.txt'
--- lib/lp/registry/browser/tests/product-views.txt	2010-08-16 19:28:37 +0000
+++ lib/lp/registry/browser/tests/product-views.txt	2010-09-17 01:08:50 +0000
@@ -482,8 +482,9 @@
 The portlet are rendered when a product officially uses the Launchpad
 Answers, Blueprints, and Bugs applications.
 
-    >>> product.official_answers = True
-    >>> product.official_blueprints = True
+    >>> from lp.app.enums import ServiceUsage
+    >>> product.answers_usage = ServiceUsage.LAUNCHPAD
+    >>> product.blueprints_usage = ServiceUsage.LAUNCHPAD
     >>> product.official_malone = True
 
     >>> view = create_initialized_view(

=== modified file 'lib/lp/registry/browser/tests/productseries-views.txt'
--- lib/lp/registry/browser/tests/productseries-views.txt	2010-09-03 06:06:40 +0000
+++ lib/lp/registry/browser/tests/productseries-views.txt	2010-09-17 01:08:50 +0000
@@ -23,24 +23,26 @@
 The ProductSeries involvement view uses the ProductSeriesInvolvedMenu when
 rendering links:
 
+    >>> from lp.app.enums import ServiceUsage
     >>> login_person(product.owner)
-    >>> product.official_answers = True
-    >>> product.official_blueprints = True
+    >>> product.answers_usage = ServiceUsage.LAUNCHPAD
+    >>> product.blueprints_usage = ServiceUsage.LAUNCHPAD
     >>> product.official_malone = True
-    >>> product.official_rosetta = True
+    >>> product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> view = create_view(series, '+get-involved')
 
-    # official_answers is always false for product series.
-    >>> print view.official_answers
-    False
-    >>> print view.official_blueprints
-    True
+    # answers_usage is never LAUNCHPAD for product series.
+    >>> print view.answers_usage.name
+    NOT_APPLICABLE
+    >>> print view.blueprints_usage.name
+    LAUNCHPAD
     >>> print view.official_malone
     True
-    >>> print view.official_rosetta
-    True
+    >>> print view.translations_usage.name
+    LAUNCHPAD
     >>> print view.codehosting_usage.name
     UNKNOWN
+
     >>> for link in view.enabled_links:
     ...     print link.url
     http://bugs.launchpad.dev/app/simple/+filebug
@@ -476,7 +478,7 @@
 A series cannot be deleted if it is has translation templates.
 
     >>> translated_series = factory.makeProductSeries(product=product)
-    >>> product.official_rosetta = True
+    >>> product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> po_template = factory.makePOTemplate(
     ...     name='gibberish', productseries=translated_series)
     >>> translated_view = create_initialized_view(

=== modified file 'lib/lp/registry/browser/tests/projectgroup-views.txt'
--- lib/lp/registry/browser/tests/projectgroup-views.txt	2009-08-31 20:46:06 +0000
+++ lib/lp/registry/browser/tests/projectgroup-views.txt	2010-09-17 01:08:50 +0000
@@ -71,12 +71,14 @@
 The portlet are rendered when a child product officially uses the Launchpad
 Answers, Blueprints, and Bugs applications.
 
-    >>> product.official_answers = True
-    >>> product.official_blueprints = True
+    >>> from lp.app.enums import ServiceUsage
+    >>> product.answers_usage = ServiceUsage.LAUNCHPAD
+    >>> product.blueprints_usage = ServiceUsage.LAUNCHPAD
     >>> product.official_malone = True
 
     >>> view = create_view(projectgroup, name='+index', principal=owner)
     >>> content = find_tag_by_id(view.render(), 'maincontent')
+
     >>> print find_tag_by_id(content, 'portlet-latest-faqs')['id']
     portlet-latest-faqs
     >>> print find_tag_by_id(content, 'portlet-latest-questions')['id']

=== modified file 'lib/lp/registry/browser/tests/test_series_views.py'
--- lib/lp/registry/browser/tests/test_series_views.py	2010-08-31 15:14:01 +0000
+++ lib/lp/registry/browser/tests/test_series_views.py	2010-09-17 01:08:50 +0000
@@ -3,14 +3,24 @@
 
 __metaclass__ = type
 
+from BeautifulSoup import BeautifulSoup
 from storm.zope.interfaces import IResultSet
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
+import unittest
+
 from canonical.config import config
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.webapp.batching import BatchNavigator
+from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.testing import LaunchpadZopelessLayer
+from lp.registry.enum import (
+    DistroSeriesDifferenceStatus,
+    DistroSeriesDifferenceType,
+    )
+from lp.services.features.flags import FeatureController
+from lp.services.features.model import FeatureFlag, getFeatureStore
 from lp.testing import TestCaseWithFactory
 from lp.testing.views import create_initialized_view
 
@@ -44,6 +54,134 @@
         self.assertEqual(view.needs_linking, None)
 
 
+class DistroSeriesLocalPackageDiffsTestCase(TestCaseWithFactory):
+    """Test the distroseries +localpackagediffs view."""
+
+    layer = LaunchpadZopelessLayer
+
+    def makeDerivedSeries(self, derived_name=None, parent_name=None):
+        # Helper that creates a derived distro series.
+        parent = self.factory.makeDistroSeries(name=parent_name)
+        derived_series = self.factory.makeDistroSeries(
+            name=derived_name, parent_series=parent)
+        return derived_series
+
+    def setDerivedSeriesUIFeatureFlag(self):
+        # Helper to set the feature flag enabling the derived series ui.
+        ignore = getFeatureStore().add(FeatureFlag(
+            scope=u'default', flag=u'soyuz.derived-series-ui.enabled',
+            value=u'on', priority=1))
+
+    def getDerivedSeriesUIFeatureFlag(self, flag):
+        """Helper to return the given flag leaving tests more readable."""
+        def in_scope(value):
+            return True
+
+        feature_controller = FeatureController(in_scope)
+        return feature_controller.getFlag(flag)
+
+    def test_view_redirects_without_feature_flag(self):
+        # If the feature flag soyuz.derived-series-ui.enabled is not set the
+        # view simply redirects to the derived series.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+
+        self.assertIs(
+            None, self.getDerivedSeriesUIFeatureFlag(
+                'soyuz.derived-series-ui.enabled'))
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        response = view.request.response
+        self.assertEqual(302, response.getStatus())
+        self.assertEqual(
+            canonical_url(derived_series), response.getHeader('location'))
+
+    def test_label(self):
+        # The view label includes the names of both series.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        self.assertEqual(
+            "Source package differences between 'Derilucid' and "
+            "parent series 'Lucid'",
+            view.label)
+
+    def test_batch_includes_needing_attention_only(self):
+        # The differences attribute includes differences needing
+        # attention only.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+        current_difference = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series)
+        old_difference = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series,
+            status=DistroSeriesDifferenceStatus.RESOLVED)
+
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        self.assertContentEqual(
+            [current_difference], view.cached_differences.batch)
+
+    def test_batch_includes_different_versions_only(self):
+        # The view contains differences of type DIFFERENT_VERSIONS only.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+        different_versions_diff = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series)
+        unique_diff = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series,
+            difference_type=(
+                DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES))
+
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        self.assertContentEqual(
+            [different_versions_diff], view.cached_differences.batch)
+
+    def test_template_includes_help_link(self):
+        # The help link for popup help is included.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+
+        self.setDerivedSeriesUIFeatureFlag()
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        soup = BeautifulSoup(view())
+        help_links = soup.findAll(
+            'a', href='/+help/soyuz/derived-series-syncing.html')
+        self.assertEqual(1, len(help_links))
+
+    def test_diff_row_includes_last_comment_only(self):
+        # The most recent comment is rendered for each difference.
+        derived_series = self.makeDerivedSeries(
+            parent_name='lucid', derived_name='derilucid')
+        difference = self.factory.makeDistroSeriesDifference(
+            derived_series=derived_series)
+        difference.addComment(difference.owner, "Earlier comment")
+        difference.addComment(difference.owner, "Latest comment")
+
+        self.setDerivedSeriesUIFeatureFlag()
+        view = create_initialized_view(
+            derived_series, '+localpackagediffs')
+
+        # Find all the rows within the body of the table
+        # listing the differences.
+        soup = BeautifulSoup(view())
+        diff_table = soup.find('table', {'class': 'listing'})
+        rows = diff_table.tbody.findAll('tr')
+
+        self.assertEqual(1, len(rows))
+        self.assertIn("Latest comment", unicode(rows[0]))
+        self.assertNotIn("Earlier comment", unicode(rows[0]))
+
+
 class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory):
     """Test the series.milestone_batch_navigator attribute."""
 
@@ -61,6 +199,7 @@
         product = self.factory.makeProduct()
         for name in ('a', 'b', 'c', 'd'):
             product.development_focus.newMilestone(name)
+
         view = create_initialized_view(
             product.development_focus, name='+index')
         self._check_milestone_batch_navigator(view)
@@ -84,3 +223,7 @@
             for item in view.milestone_batch_navigator.currentBatch()]
         self.assertEqual(expected, milestone_names)
         config.pop('default-batch-size')
+
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py	2010-03-05 13:56:38 +0000
+++ lib/lp/registry/browser/tests/test_team.py	2010-09-17 01:08:50 +0000
@@ -3,10 +3,12 @@
 
 __metaclass__ = type
 
+from canonical.launchpad.webapp.batching import BatchNavigator
 from canonical.testing import DatabaseFunctionalLayer
 from lp.registry.browser.person import TeamOverviewMenu
 from lp.testing import TestCaseWithFactory
 from lp.testing.menu import check_menu_links
+from lp.testing.views import create_initialized_view
 
 
 class TestTeamMenu(TestCaseWithFactory):
@@ -35,3 +37,22 @@
         self.assertEqual(True, check_menu_links(menu))
         link = menu.configure_mailing_list()
         self.assertEqual('Configure mailing list', link.text)
+
+
+class TestModeration(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_held_messages_is_batch_navigator(self):
+        team = self.factory.makeTeam()
+        self.factory.makeMailingList(team, team.teamowner)
+        view = create_initialized_view(team, name='+mailinglist-moderate')
+        self.assertIsInstance(view.held_messages, BatchNavigator)
+
+    def test_held_message_headings(self):
+        team = self.factory.makeTeam()
+        self.factory.makeMailingList(team, team.teamowner)
+        view = create_initialized_view(team, name='+mailinglist-moderate')
+        navigator = view.held_messages
+        self.assertEqual('message', navigator._singular_heading)
+        self.assertEqual('messages', navigator._plural_heading)

=== added file 'lib/lp/registry/browser/tests/test_team_view.py'
--- lib/lp/registry/browser/tests/test_team_view.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_team_view.py	2010-09-17 01:08:50 +0000
@@ -0,0 +1,143 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""
+Test team views.
+"""
+
+__metaclass__ = type
+
+import transaction
+
+from canonical.testing import DatabaseFunctionalLayer
+
+from lp.registry.interfaces.person import TeamSubscriptionPolicy
+
+from lp.testing import (
+    login_person,
+    TestCaseWithFactory,
+    )
+from lp.testing.views import create_initialized_view
+
+
+class TestProposedTeamMembersEditView(TestCaseWithFactory):
+    """Tests for ProposedTeamMembersEditView."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestProposedTeamMembersEditView, self).setUp()
+        self.owner = self.factory.makePerson(name="team-owner")
+        self.a_team = self.makeTeam("team-a", "A-Team")
+        self.b_team = self.makeTeam("team-b", "B-Team")
+        transaction.commit()
+        login_person(self.owner)
+
+    def makeTeam(self, name, displayname):
+        """Make a moderated team."""
+        return self.factory.makeTeam(
+            name=name,
+            owner=self.owner,
+            displayname=displayname,
+            subscription_policy=TeamSubscriptionPolicy.MODERATED)
+
+    def inviteToJoin(self, joinee, joiner):
+        """Invite the joiner team into the joinee team."""
+        # Joiner is proposed to join joinee.
+        form = {
+            'field.teams': joiner.name,
+            'field.actions.continue': 'Continue',
+            }
+        view = create_initialized_view(
+            joinee, "+add-my-teams", form=form)
+        self.assertEqual([], view.errors)
+        notifications = view.request.response.notifications
+        self.assertEqual(1, len(notifications))
+        expected = u"%s has been proposed to this team." % (
+            joiner.displayname)
+        self.assertEqual(
+            expected,
+            notifications[0].message)
+
+    def acceptTeam(self, joinee, successful, failed):
+        """Accept the teams into the joinee team.
+
+        The teams in 'successful' are expected to be allowed.
+        The teams in 'failed' are expected to fail.
+        """
+        failed_names = ', '.join([team.displayname for team in failed])
+        if len(failed) == 1:
+            failed_message = (
+                u'%s is a member of the following team, '
+                'so it could not be accepted:  %s.  '
+                'You need to "Decline" that team.' %
+                (joinee.displayname, failed_names))
+        else:
+            failed_message = (
+                u'%s is a member of the following teams, '
+                'so they could not be accepted:  %s.  '
+                'You need to "Decline" those teams.' %
+                (joinee.displayname, failed_names))
+
+        form = {
+            'field.actions.save': 'Save changes',
+            }
+        for team in successful + failed:
+            # Construct the team selection field, based on the id of the
+            # team.
+            selector = 'action_%d' % team.id
+            form[selector] = 'approve'
+
+        view = create_initialized_view(
+            joinee, "+editproposedmembers", form=form)
+        self.assertEqual([], view.errors)
+        notifications = view.request.response.notifications
+        if len(failed) == 0:
+            self.assertEqual(0, len(notifications))
+        else:
+            self.assertEqual(1, len(notifications))
+            self.assertEqual(
+                failed_message,
+                notifications[0].message)
+
+    def test_circular_proposal_acceptance(self):
+        """Two teams can invite each other without horrifying results."""
+
+        # Make the criss-cross invitations.
+
+        # Owner proposes Team B join Team A.
+        self.inviteToJoin(self.a_team, self.b_team)
+
+        # Owner proposes Team A join Team B.
+        self.inviteToJoin(self.b_team, self.a_team)
+
+        # Accept Team B into Team A.
+        self.acceptTeam(self.a_team, successful=(self.b_team,), failed=())
+
+        # Accept Team A into Team B, and fail trying.
+        self.acceptTeam(self.b_team, successful=(), failed=(self.a_team,))
+
+    def test_circular_proposal_acceptance_with_some_noncircular(self):
+        """Accepting a mix of successful and failed teams works."""
+        # Create some extra teams.
+        self.c_team = self.makeTeam("team-c", "C-Team")
+        self.d_team = self.makeTeam("team-d", "D-Team")
+        self.super_team = self.makeTeam("super-team", "Super Team")
+
+        # Everyone wants to join Super Team.
+        for team in [self.a_team, self.b_team, self.c_team, self.d_team]:
+            self.inviteToJoin(self.super_team, team)
+
+        # Super Team joins two teams.
+        for team in [self.a_team, self.b_team]:
+            self.inviteToJoin(team, self.super_team)
+
+        # Super Team is accepted into both.
+        for team in [self.a_team, self.b_team]:
+            self.acceptTeam(team, successful=(self.super_team, ), failed=())
+
+        # Now Super Team attempts to accept all teams.  Two succeed but the
+        # two with that would cause a cycle fail.
+        failed = (self.a_team, self.b_team)
+        successful = (self.c_team, self.d_team)
+        self.acceptTeam(self.super_team, successful, failed)

=== modified file 'lib/lp/registry/doc/distribution.txt'
--- lib/lp/registry/doc/distribution.txt	2010-08-23 00:51:30 +0000
+++ lib/lp/registry/doc/distribution.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= Distributions =
+Distributions
+=============
 
 From the DerivationOverview spec
 <https://launchpad.canonical.com/DerivationOverview>:
@@ -143,7 +144,8 @@
     False
 
 
-== Distribution Sorting ==
+Distribution Sorting
+--------------------
 
 If you ask for all the distributions in the DistributionSet you should get
 Ubuntu (and all flavours of it) first and the rest alphabetically:
@@ -164,7 +166,8 @@
     True
 
 
-=== Searching for DistributionSourcePackages ===
+Searching for DistributionSourcePackages
+........................................
 
 The distribution also allows you to look for source packages that match
 a certain string through the magic of fti. For instance:
@@ -232,7 +235,8 @@
     DistributionSourcePackage: alsa-utils
 
 
-=== Searching for binary packages ===
+Searching for binary packages
+.............................
 
 There are two useful functions for searching binary packages related
 to the distribution, searchBinaryPackages() and searchBinaryPackagesFTI().
@@ -291,7 +295,8 @@
     [u'mozilla-firefox']
 
 
-=== Finding distroseriess and pockets from distribution names ===
+Finding distroseriess and pockets from distribution names
+.........................................................
 
 A distribution knows what distroseriess it has. Those distroseriess have
 pockets which have suffixes used by the archive publisher. Because we
@@ -336,26 +341,27 @@
     NotFoundError: 'hoary-bullshit'
 
 
-=== Upload related stuff ===
+Upload related stuff
+....................
 
 When uploading to a distribution we need to query its uploaders. Each
 uploader record is in fact an ArchivePermission record that tells us
 what component is uploadable to by what person or group of people.
 
-   >>> from operator import attrgetter
-   >>> for permission in sorted(
-   ...     ubuntu.uploaders, key=attrgetter("id")):
-   ...     assert not permission.archive.is_ppa
-   ...     print permission.component.name
-   ...     print permission.person.displayname
-   universe
-   Ubuntu Team
-   restricted
-   Ubuntu Team
-   main
-   Ubuntu Team
-   partner
-   Canonical Partner Developers
+    >>> from operator import attrgetter
+    >>> for permission in sorted(
+    ...     ubuntu.uploaders, key=attrgetter("id")):
+    ...     assert not permission.archive.is_ppa
+    ...     print permission.component.name
+    ...     print permission.person.displayname
+    universe
+    Ubuntu Team
+    restricted
+    Ubuntu Team
+    main
+    Ubuntu Team
+    partner
+    Canonical Partner Developers
 
 When processing an upload we may want to find a file (E.g. if an
 incomplete source is uploaded).
@@ -375,7 +381,8 @@
     AssertionError: searching in an explicitly empty space is pointless
 
 
-=== Launchpad Usage ===
+Launchpad Usage
+...............
 
 A distribution can specify if it uses Malone, Rosetta, or Answers
 officially. Ubuntu uses all of them:
@@ -385,24 +392,24 @@
     >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
     >>> ubuntu.official_malone
     True
-    >>> ubuntu.official_rosetta
-    True
-    >>> ubuntu.official_answers
-    True
-    >>> ubuntu.official_blueprints
-    True
+    >>> print ubuntu.answers_usage.name
+    LAUNCHPAD
+    >>> print ubuntu.blueprints_usage.name
+    LAUNCHPAD
+    >>> print ubuntu.translations_usage.name
+    LAUNCHPAD
 
 The bug_tracking_usage property currently only tracks official_malone.
 
     >>> print ubuntu.bug_tracking_usage.name
     LAUNCHPAD
 
-While the other attributes track the other official_ attributes. 
+While the other attributes track the other official_ attributes.
 
     >>> print ubuntu.official_rosetta
     True
     >>> print ubuntu.translations_usage.name
-    LAUNCHPAD 
+    LAUNCHPAD
     >>> print ubuntu.official_answers
     True
     >>> print ubuntu.answers_usage.name
@@ -412,7 +419,8 @@
     >>> print ubuntu.blueprints_usage.name
     LAUNCHPAD
 
-If the official_ attributes are False, the usage enums don't know anything.
+If the official_ attributes are False and the enum hasn't been set,
+the usage enums don't know anything.
 
     >>> login_person(ubuntu.owner.teamowner)
     >>> ubuntu.official_rosetta = False
@@ -422,74 +430,74 @@
 A distribution *cannot* specify that it uses codehosting. Currently there's
 no way for a distribution to use codehosting.
 
-    >>> ubuntu.official_codehosting
-    False
-    >>> ubuntu.official_codehosting = True
-    Traceback (most recent call last):
-    ForbiddenAttribute: ('official_codehosting', ...)
-
+    >>> from lp.app.enums import ServiceUsage
     >>> print ubuntu.codehosting_usage.name
     NOT_APPLICABLE
+    >>> ubuntu.codehosting_usage = ServiceUsage.LAUNCHPAD
+    Traceback (most recent call last):
+    AttributeError: can't set attribute...
 
 While Debian uses none:
 
     >>> debian = getUtility(ILaunchpadCelebrities).debian
-    >>> debian.official_malone
-    False
-    >>> debian.official_rosetta
-    False
-    >>> debian.official_answers
-    False
-    >>> debian.official_codehosting
-    False
-    >>> debian.official_blueprints
-    False
+    >>> print debian.bug_tracking_usage.name
+    UNKNOWN
+    >>> print debian.translations_usage.name
+    UNKNOWN
+    >>> print debian.answers_usage.name
+    UNKNOWN
+    >>> print debian.codehosting_usage.name
+    NOT_APPLICABLE
+    >>> print debian.blueprints_usage.name
+    UNKNOWN
 
 Gentoo only uses Malone
 
-    >>> print gentoo.official_malone
-    True
-    >>> print gentoo.official_rosetta
-    False
-    >>> print gentoo.official_answers
-    False
+    >>> print gentoo.bug_tracking_usage.name
+    LAUNCHPAD
+    >>> print gentoo.translations_usage.name
+    UNKNOWN
+    >>> print gentoo.answers_usage.name
+    UNKNOWN
 
 Launchpad admins and the distro owner can set these fields.
 
+    >>> from lp.app.enums import ServiceUsage
     >>> login('mark@xxxxxxxxxxx')
     >>> debian = getUtility(ILaunchpadCelebrities).debian
-    >>> debian.official_blueprints = True
-    >>> debian.official_blueprints
-    True
+    >>> debian.blueprints_usage = ServiceUsage.LAUNCHPAD
+    >>> print debian.blueprints_usage.name
+    LAUNCHPAD
     >>> debian.official_malone = True
     >>> debian.official_malone
     True
-    >>> debian.official_rosetta = True
-    >>> debian.official_rosetta
-    True
+    >>> debian.translations_usage = ServiceUsage.LAUNCHPAD
+    >>> debian.translations_usage.name
+    'LAUNCHPAD'
 
     >>> debian_owner = factory.makePerson()
     >>> debian.owner = debian_owner
     >>> login_person(debian_owner)
-    >>> debian.official_blueprints = False
-    >>> debian.official_blueprints
-    False
+    >>> debian.blueprints_usage = ServiceUsage.NOT_APPLICABLE
+    >>> print debian.blueprints_usage.name
+    NOT_APPLICABLE
 
 But others can't.
 
     >>> login('no-priv@xxxxxxxxxxxxx')
-    >>> debian.official_blueprints = True
+    >>> debian.blueprints_usage = ServiceUsage.LAUNCHPAD
     Traceback (most recent call last):
-    Unauthorized: (..., 'official_blueprints', 'launchpad.Edit')
+    Unauthorized: (..., 'blueprints_usage', 'launchpad.Edit')
     >>> debian.official_malone = True
     Traceback (most recent call last):
     Unauthorized: (..., 'official_malone', 'launchpad.Edit')
-    >>> debian.official_rosetta = True
+    >>> debian.translations_usage = ServiceUsage.LAUNCHPAD
     Traceback (most recent call last):
-    Unauthorized: (..., 'official_rosetta', 'launchpad.Edit')
-
-
-=== Specification Listings ===
+    Unauthorized: (..., 'translations_usage', 'launchpad.Edit')
+
+
+Specification Listings
+......................
 
 We should be able to get lists of specifications in different states
 related to a distro.
@@ -497,90 +505,93 @@
 Basically, we can filter by completeness, and by whether or not the spec is
 informational.
 
- >>> kubuntu = distroset.getByName("kubuntu")
+    >>> kubuntu = distroset.getByName("kubuntu")
 
- >>> from canonical.launchpad.interfaces import SpecificationFilter
+    >>> from canonical.launchpad.interfaces import SpecificationFilter
 
 First, there should be one informational spec for kubuntu, but it is
 complete so it will not show up unless we explicitly ask for complete specs:
 
- >>> filter = [SpecificationFilter.INFORMATIONAL]
- >>> kubuntu.specifications(filter=filter).count()
- 0
- >>> filter = [SpecificationFilter.INFORMATIONAL,
- ...           SpecificationFilter.COMPLETE]
- >>> kubuntu.specifications(filter=filter).count()
- 1
+    >>> filter = [SpecificationFilter.INFORMATIONAL]
+    >>> kubuntu.specifications(filter=filter).count()
+    0
+    >>> filter = [SpecificationFilter.INFORMATIONAL,
+    ...           SpecificationFilter.COMPLETE]
+    >>> kubuntu.specifications(filter=filter).count()
+    1
 
 
 There are 2 completed specs for Kubuntu:
 
- >>> filter = [SpecificationFilter.COMPLETE]
- >>> for spec in kubuntu.specifications(filter=filter):
- ...    print spec.name, spec.is_complete
- thinclient-local-devices True
- usplash-on-hibernation True
+    >>> filter = [SpecificationFilter.COMPLETE]
+    >>> for spec in kubuntu.specifications(filter=filter):
+    ...    print spec.name, spec.is_complete
+    thinclient-local-devices True
+    usplash-on-hibernation True
 
 
 And there are four incomplete specs:
 
- >>> filter = [SpecificationFilter.INCOMPLETE]
- >>> for spec in kubuntu.specifications(filter=filter):
- ...    print spec.name, spec.is_complete
- cluster-installation False
- revu False
- kde-desktopfile-langpacks False
- krunch-desktop-plan False
+    >>> filter = [SpecificationFilter.INCOMPLETE]
+    >>> for spec in kubuntu.specifications(filter=filter):
+    ...    print spec.name, spec.is_complete
+    cluster-installation False
+    revu False
+    kde-desktopfile-langpacks False
+    krunch-desktop-plan False
 
 
 If we ask for all specs, we get them in the order of priority.
 
- >>> filter = [SpecificationFilter.ALL]
- >>> for spec in kubuntu.specifications(filter=filter):
- ...    print spec.priority.title, spec.name
- Essential cluster-installation
- High revu
- Medium thinclient-local-devices
- Low usplash-on-hibernation
- Undefined kde-desktopfile-langpacks
- Not krunch-desktop-plan
+    >>> filter = [SpecificationFilter.ALL]
+    >>> for spec in kubuntu.specifications(filter=filter):
+    ...    print spec.priority.title, spec.name
+    Essential cluster-installation
+    High revu
+    Medium thinclient-local-devices
+    Low usplash-on-hibernation
+    Undefined kde-desktopfile-langpacks
+    Not krunch-desktop-plan
 
 
 And if we ask just for specs, we get the incomplete ones.
 
- >>> for spec in kubuntu.specifications():
- ...     print spec.name, spec.is_complete
- cluster-installation False
- revu False
- kde-desktopfile-langpacks False
- krunch-desktop-plan False
+    >>> for spec in kubuntu.specifications():
+    ...     print spec.name, spec.is_complete
+    cluster-installation False
+    revu False
+    kde-desktopfile-langpacks False
+    krunch-desktop-plan False
 
 We can filter for specifications that contain specific text:
 
- >>> for spec in kubuntu.specifications(filter=['package']):
- ...     print spec.name
- revu
+    >>> for spec in kubuntu.specifications(filter=['package']):
+    ...     print spec.name
+    revu
 
 We can get only valid specs (those that are not obsolete or superseded):
 
- >>> from canonical.launchpad.interfaces import SpecificationDefinitionStatus
- >>> login('mark@xxxxxxxxxxx')
- >>> for spec in kubuntu.specifications():
- ...     # Do this here, otherwise, the change will be flush before
- ...     # updateLifecycleStatus() acts and an IntegrityError will be
- ...     # raised.
- ...     owner = spec.owner
- ...     if spec.name in ['cluster-installation', 'revu']:
- ...         spec.definition_status = SpecificationDefinitionStatus.OBSOLETE
- ...     if spec.name in ['krunch-desktop-plan']:
- ...         spec.definition_status = SpecificationDefinitionStatus.SUPERSEDED
- ...     shim = spec.updateLifecycleStatus(owner)
- >>> for spec in kubuntu.valid_specifications:
- ...     print spec.name
- kde-desktopfile-langpacks
-
-
-== Milestones ==
+    >>> from canonical.launchpad.interfaces import (
+    ...     SpecificationDefinitionStatus,
+    ...     )
+    >>> login('mark@xxxxxxxxxxx')
+    >>> for spec in kubuntu.specifications():
+    ...     # Do this here, otherwise, the change will be flush before
+    ...     # updateLifecycleStatus() acts and an IntegrityError will be
+    ...     # raised.
+    ...     owner = spec.owner
+    ...     if spec.name in ['cluster-installation', 'revu']:
+    ...         spec.definition_status = SpecificationDefinitionStatus.OBSOLETE
+    ...     if spec.name in ['krunch-desktop-plan']:
+    ...         spec.definition_status = SpecificationDefinitionStatus.SUPERSEDED
+    ...     shim = spec.updateLifecycleStatus(owner)
+    >>> for spec in kubuntu.valid_specifications:
+    ...     print spec.name
+    kde-desktopfile-langpacks
+
+
+Milestones
+----------
 
 We can use IDistribution.milestones to get all milestones associated with any
 series of a distribution.
@@ -623,7 +634,8 @@
     [u'3.1', u'3.1-rc1', u'woody-rc1']
 
 
-== Archives ==
+Archives
+--------
 
 A distribution archive (primary, partner, debug or copy) can be retrieved
 by name using IDistribution.getArchive.

=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt	2010-08-27 12:17:15 +0000
+++ lib/lp/registry/doc/person.txt	2010-09-17 01:08:50 +0000
@@ -628,31 +628,6 @@
     >>> vcs_imports.hasParticipationEntryFor(vcs_imports)
     True
 
-The inTeam method is cached to avoid unnecessary database lookups - this
-was a cause of a number of timeouts
-
-    >>> naked_lifeless = removeSecurityProxy(lifeless)
-    >>> naked_lifeless._inTeam_cache[vcs_imports.id]
-    True
-
-    >>> naked_lifeless._inTeam_cache[vcs_imports.id] = False
-    >>> lifeless.inTeam(vcs_imports)
-    False
-
-    >>> naked_lifeless._inTeam_cache[vcs_imports.id] = True
-    >>> lifeless.inTeam(vcs_imports)
-    True
-
-IPerson has a method to clear it; this is used between IPerson instances
-to ensure that the cache can be kept consistent when membership changes.
-
-    >>> naked_lifeless.clearInTeamCache()
-    >>> naked_lifeless._inTeam_cache
-    {}
-
-** See lib/canonical/launchpad/doc/teammembership.txt for more
-information about team membership/participation.
-
 
 Email notifications to teams
 ............................

=== modified file 'lib/lp/registry/doc/product.txt'
--- lib/lp/registry/doc/product.txt	2010-08-25 15:48:05 +0000
+++ lib/lp/registry/doc/product.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= Product =
+Product
+=======
 
 Launchpad keeps track of the "upstream" world as well as the "distro" world.
 The anchorpiece of the "upstream" world is the Product, which is a piece of
@@ -162,7 +163,8 @@
     Obsolete Junk
 
 
-== Translatable Products ==
+Translatable Products
+---------------------
 
 IProductSet will also tell us which products can be translated:
 
@@ -217,11 +219,11 @@
 The packaging table allows us to list source and distro source packages
 related to a certain upstream:
 
-   >>> alsa = productset.getByName('alsa-utils')
-   >>> [(sp.name, sp.distroseries.name) for sp in alsa.sourcepackages]
-   [(u'alsa-utils', u'sid'), (u'alsa-utils', u'warty')]
-   >>> [(sp.name, sp.distribution.name) for sp in alsa.distrosourcepackages]
-   [(u'alsa-utils', u'debian'), (u'alsa-utils', u'ubuntu')]
+    >>> alsa = productset.getByName('alsa-utils')
+    >>> [(sp.name, sp.distroseries.name) for sp in alsa.sourcepackages]
+    [(u'alsa-utils', u'sid'), (u'alsa-utils', u'warty')]
+    >>> [(sp.name, sp.distribution.name) for sp in alsa.distrosourcepackages]
+    [(u'alsa-utils', u'debian'), (u'alsa-utils', u'ubuntu')]
 
 The date_next_suggest_packaging attribute records the date when Launchpad can
 resume suggesting Ubuntu packages that the project provides. A value of None
@@ -270,7 +272,8 @@
     >>> from canonical.launchpad.interfaces import IBugTrackerSet
 
     >>> login_person(firefox.owner)
-    >>> gnome_bugzilla = getUtility(IBugTrackerSet).getByName('gnome-bugzilla')
+    >>> bug_tracker_set = getUtility(IBugTrackerSet)
+    >>> gnome_bugzilla = bug_tracker_set.getByName('gnome-bugzilla')
     >>> firefox.project.bugtracker = gnome_bugzilla
     >>> firefox.getExternalBugTracker() is None
     True
@@ -307,21 +310,22 @@
     True
 
 
-== Answer Tracking ==
+Answer Tracking
+---------------
 
 Firefox uses the Answer Tracker as the official application to provide
 answers to questions.
 
-    >>> firefox.official_answers
-    True
+    >>> print firefox.answers_usage.name
+    LAUNCHPAD
 
 Alsa does not use Launchpad to track answers.
 
-    >>> alsa.official_answers
-    False
-
-
-== Product Creation ==
+    >>> print alsa.answers_usage.name
+    UNKNOWN
+
+Product Creation
+----------------
 
 We can create new products with the createProduct() method:
 
@@ -374,20 +378,22 @@
     True
 
 
-== Specification Listings ==
+Specification Listings
+----------------------
 
 We should be able to set whether or not a Product uses specifications
-officially.  It defaults to False.
-
- >>> firefox = productset.getByName('firefox')
- >>> firefox.official_blueprints
- False
-
-We can change it to True.
-
- >>> firefox.official_blueprints = True
- >>> firefox.official_blueprints
- True
+officially.  It defaults to UNKNOWN.
+
+    >>> firefox = productset.getByName('firefox')
+    >>> print firefox.blueprints_usage.name
+    UNKNOWN
+
+We can change it to use LAUNCHPAD.
+
+    >>> from lp.app.enums import ServiceUsage
+    >>> firefox.blueprints_usage = ServiceUsage.LAUNCHPAD
+    >>> print firefox.blueprints_usage.name
+    LAUNCHPAD
 
 We should be able to get lists of specifications in different states
 related to a product.
@@ -395,39 +401,40 @@
 Basically, we can filter by completeness, and by whether or not the spec is
 informational.
 
- >>> firefox = productset.getByName('firefox')
- >>> from canonical.launchpad.interfaces import SpecificationFilter
+    >>> firefox = productset.getByName('firefox')
+    >>> from canonical.launchpad.interfaces import SpecificationFilter
 
 First, there should be only one informational spec for firefox:
 
- >>> filter = [SpecificationFilter.INFORMATIONAL]
- >>> for spec in firefox.specifications(filter=filter):
- ...    print spec.name
- extension-manager-upgrades
+    >>> filter = [SpecificationFilter.INFORMATIONAL]
+    >>> for spec in firefox.specifications(filter=filter):
+    ...    print spec.name
+    extension-manager-upgrades
 
 
 There are no completed specs for firefox:
 
- >>> filter = [SpecificationFilter.COMPLETE]
- >>> for spec in firefox.specifications(filter=filter):
- ...    print spec.name
+    >>> filter = [SpecificationFilter.COMPLETE]
+    >>> for spec in firefox.specifications(filter=filter):
+    ...    print spec.name
 
 
 And there are five incomplete specs:
 
- >>> filter = [SpecificationFilter.INCOMPLETE]
- >>> firefox.specifications(filter=filter).count()
- 5
+    >>> filter = [SpecificationFilter.INCOMPLETE]
+    >>> firefox.specifications(filter=filter).count()
+    5
 
 We can filter for specifications that contain specific text:
 
- >>> for spec in firefox.specifications(filter=['new']):
- ...     print spec.name
- canvas
- e4x
-
-
-== Milestones ==
+    >>> for spec in firefox.specifications(filter=['new']):
+    ...     print spec.name
+    canvas
+    e4x
+
+
+Milestones
+----------
 
 We can use IProduct.milestones to get all milestones associated with any
 ProductSeries of a product.
@@ -470,7 +477,8 @@
     [u'1.0.0', u'0.9.2', u'0.9.1', u'0.9', u'1.0', u'1.0-rc1']
 
 
-== Release ==
+Release
+-------
 
 All the releases for a Product can be retrieved through the releases property.
 
@@ -489,7 +497,8 @@
     0.9.1
 
 
-== Products With Branches ==
+Products With Branches
+----------------------
 
 Products are considered to officially support Launchpad as a location
 for their branches after a branch is set for the development focus
@@ -568,7 +577,8 @@
     landscape
 
 
-== Primary translatable ==
+Primary translatable
+--------------------
 
 Primary translatable series in a product should follow series where
 development is focused on.  To be able to do changes to facilitate
@@ -583,12 +593,18 @@
     >>> from lp.translations.interfaces.potemplate import IPOTemplateSet
     >>> potemplate_set = getUtility(IPOTemplateSet)
 
+We're going to be setting the ServiceUsage values for products, so we
+need those enums.
+
+    >>> from lp.app.enums import ServiceUsage
+
 Firefox has two series, but no translatable series either:
 
     >>> firefox = productset.getByName('firefox')
     >>> for firefoxseries in firefox.series:
-    ...     print '%s %s' % (firefoxseries.displayname,
-    ...                      list(firefoxseries.getCurrentTranslationTemplates()))
+    ...     print '%s %s' % (
+    ...         firefoxseries.displayname,
+    ...         list(firefoxseries.getCurrentTranslationTemplates()))
     1.0 []
     trunk []
     >>> print firefox.primary_translatable
@@ -616,7 +632,7 @@
 And set that product as using translations officially. We need it so
 translations are available.
 
-    >>> firefox.official_rosetta = True
+    >>> firefox.translations_usage = ServiceUsage.LAUNCHPAD
 
 The primary_translatable now points at firefox 1.0:
 
@@ -642,7 +658,8 @@
     1.0
 
 
-= Series list =
+Series list
+===========
 
 The series for a product are returned as a sorted list, with the
 exception that the current development focus is first.
@@ -697,7 +714,8 @@
     ...     print series.name
     trunk
 
-= Changing ownership =
+Changing ownership
+==================
 
 If the owner of a project changes, all series and productreleases
 owned by the old owner are transfered to the new owner.

=== modified file 'lib/lp/registry/doc/project.txt'
--- lib/lp/registry/doc/project.txt	2010-06-07 07:52:45 +0000
+++ lib/lp/registry/doc/project.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= ProjectGroups =
+ProjectGroups
+=============
 
 A ProjectGroup is a group of Products, making it possible to for
 example see all bugs in the ProjectGroup's Product, or make them share a
@@ -11,7 +12,8 @@
     >>> projectset = getUtility(IProjectGroupSet)
 
 
-== Creating new projects ==
+Creating new projects
+---------------------
 
 When creating a new project there are a bunch of things we need to provide.
 While some of them (homepageurl, icon, logo and mugshot) are optional, others
@@ -28,7 +30,8 @@
     u'project-test'
 
 
-== Looking up existing projects ==
+Looking up existing projects
+----------------------------
 
 To fetch a project we use IProjectGroupSet.getByName() or
 IProjectGroupSet.__getitem__. The former will, by default, return active and
@@ -98,7 +101,8 @@
     >>> print projectgroups.getByName('gnome', ignore_inactive=True)
     None
 
-== Products which are part of a project ==
+Products which are part of a project
+------------------------------------
 
 The products which are part of a given project are given by a project's
 .products property. Note that only active products are included and they're
@@ -122,7 +126,8 @@
     >>> flush_database_updates()
 
 
-== Specification Listings ==
+Specification Listings
+----------------------
 
 We should be able to generate filtered lists of specs on a project.
 
@@ -200,7 +205,8 @@
     e4x
 
 
-== Specification Listings for a ProjectGroupSeries ==
+Specification Listings for a ProjectGroupSeries
+-----------------------------------------------
 
 An IProjectGroupSeries object can be retrieved by IProjectGroup.getSeries.
 
@@ -314,7 +320,8 @@
     e4x
 
 
-== translatables ==
+Translatables
+-------------
 
 A project would have IProduct objects that have resources to translate. This
 method return us the ones that are translatable and officially using Rosetta
@@ -349,8 +356,8 @@
 
 That is using Rosetta officially.
 
-    >>> evolution.official_rosetta
-    True
+    >>> print evolution.translations_usage.name
+    LAUNCHPAD
 
 GNOME project has also another product, netapplet.
 
@@ -361,8 +368,8 @@
 But it was not returned from 'translatables' method because it's not using
 Rosetta officially.
 
-    >>> netapplet.official_rosetta
-    False
+    >>> print netapplet.translations_usage.name
+    UNKNOWN
 
 And thus, it doesn't have any translatable series.
 
@@ -376,7 +383,8 @@
     1
 
 
-== Milestones ==
+Milestones
+----------
 
 A project can have virtual milestones. If any of its products has milestones,
 these milestones are also associated with the project.
@@ -398,7 +406,8 @@
     1.1. active: True
     1.1 active: True
 
-ProjectGroup.all_milestones is a list of all milestones associated with a project.
+ProjectGroup.all_milestones is a list of all milestones associated with a
+project.
 
     >>> milestones = gnome.all_milestones
     >>> for milestone in milestones:
@@ -410,8 +419,8 @@
     1.1. active: True
     1.1 active: True
 
-ProjectGroup.getMilestone(name) returns the project milestone with the name `name'
-or None, if no milestone with this name exists.
+ProjectGroup.getMilestone(name) returns the project milestone with the name
+`name' or None, if no milestone with this name exists.
 
     >>> milestone = gnome.getMilestone('1.1')
     >>> print milestone.name

=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt	2010-08-23 08:46:04 +0000
+++ lib/lp/registry/doc/teammembership.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= Team Membership/Participation =
+Team Membership/Participation
+=============================
 
 When a person joins a team, we store the relationship in the TeamMembership
 table. In this table we store the membership status, the join date and the
@@ -57,7 +58,8 @@
     >>> flush_database_updates()
 
 
-== Adding new members ==
+Adding new members
+------------------
 
 One way of adding new members to a team is by having the user himself join the
 team he wants.
@@ -138,7 +140,8 @@
 to a team.
 
     >>> mark = personset.getByName('mark')
-    >>> t3.addMember(salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN)
+    >>> t3.addMember(salgado, reviewer=mark,
+    ...     status=TeamMembershipStatus.ADMIN)
     Traceback (most recent call last):
     ...
     Unauthorized:...
@@ -414,7 +417,8 @@
     True
 
 
-== Changing membership data ==
+Changing membership data
+------------------------
 
 The only bits of a TeamMembership that can be changed are its status, expiry
 date, reviewer[comment] and the date the user joined. From these ones, the
@@ -552,7 +556,8 @@
     AssertionError: ...
 
 
-== Flagging expired memberships ==
+Flagging expired memberships
+----------------------------
 
 The expired memberships are flagged by a cronscript that runs daily. This
 script simply flags all active memberships which reached their expiry date as
@@ -631,7 +636,6 @@
 be notified that the membership has already been automatically renewed
 on the expiration day.
 
-    >>> from storm.store import Store
     >>> autorenewal_team = factory.makeTeam(name="autorenewal-team")
     >>> autorenewal_team.renewal_policy = (
     ...     TeamMembershipRenewalPolicy.AUTOMATIC)
@@ -684,7 +688,7 @@
     DEBUG   ...
     DEBUG   Sent warning email to name16 in launchpad-buildd-admins team.
     DEBUG   Sent warning email to name12 in landscape-developers team.
-    DEBUG   Removing lock file: /var/lock/launchpad-flag-expired-memberships.lock
+    DEBUG   Removing lock file: ...launchpad-flag-expired-memberships.lock
     >>> process.returncode
     0
     >>> transaction.abort()
@@ -713,7 +717,8 @@
     datetime.timedelta(73)
 
 
-== Renewing team memberships ==
+Renewing team memberships
+-------------------------
 
 A team membership can be renewed before it has been expired by either
 changing its dateexpires (which can be done only by admins of the
@@ -786,7 +791,8 @@
     Approved
 
 
-== Querying team memberships ==
+Querying team memberships
+-------------------------
 
 You can check a person's direct memberships by using team_memberships:
 
@@ -833,7 +839,8 @@
     [u'Celso Providelo (cprov)', u'Guilherme Salgado (salgado)']
 
 
-== Finding team administrators ==
+Finding team administrators
+---------------------------
 
 Another convenient method is getDirectAdministrators(), which returns the
 admin members plus the owner in case he is not one of the admin members.
@@ -878,7 +885,8 @@
 
     >>> cprov_team = factory.makeTeam(owner=cprov, name="cprov-team")
     >>> [team.name for team in cprov.getAdministratedTeams()]
-    [u'canonical-partner-dev', u'cprov-team', u'guadamen', u'launchpad-buildd-admins']
+    [u'canonical-partner-dev', u'cprov-team', u'guadamen',
+     u'launchpad-buildd-admins']
 
 If a team is merged it will not show up in the set of administered teams.
 
@@ -889,7 +897,8 @@
     [u'canonical-partner-dev', u'guadamen', u'launchpad-buildd-admins']
 
 
-== Querying a person for team participation ==
+Querying a person for team participation
+----------------------------------------
 
 Team membership is direct; team participation is indirect, people being
 participants of teams by virtue of being members of other teams which are in
@@ -942,44 +951,8 @@
     t4
 
 
-== Membership caches ==
-
-Person instances have membership caches (_inTeam_cache) to avoid the
-need to reissue identical membership queries repeatedly. (This test uses
-Person directly to allow us access to an unproxied object, since
-_inTeam_cache is private, and it uses TeamMembershipSet directly to allow
-us to call TeamMembership.setStatus, which is private).
-
-    >>> from lp.registry.model.person import Person
-    >>> from lp.registry.model.teammembership import TeamMembershipSet
-    >>> no_priv = Person.selectOneBy(name='no-priv')
-    >>> no_priv.inTeam(admins)
-    False
-    >>> no_priv._inTeam_cache
-    {...25: False...}
-
-This cache is cleared when memberships are created:
-
-    >>> membership = TeamMembershipSet().new(
-    ...     no_priv, admins, TeamMembershipStatus.APPROVED, admins.teamowner)
-    >>> no_priv._inTeam_cache
-    {}
-    >>> no_priv.inTeam(admins)
-    True
-    >>> no_priv._inTeam_cache
-    {25: True}
-
-Or changed:
-
-    >>> membership.setStatus(TeamMembershipStatus.DEACTIVATED, mark)
-    True
-    >>> no_priv._inTeam_cache
-    {}
-    >>> no_priv.inTeam(admins)
-    False
-
-
-== Email to invalid users ==
+Email to invalid users
+----------------------
 
 There are cases where teams have deactivated or suspended users as members.
 This is a data error that needs fixing, but should not interfere with

=== modified file 'lib/lp/registry/model/distroseriesdifference.py'
--- lib/lp/registry/model/distroseriesdifference.py	2010-09-01 12:23:01 +0000
+++ lib/lp/registry/model/distroseriesdifference.py	2010-09-17 01:08:50 +0000
@@ -10,6 +10,7 @@
     ]
 
 from lazr.enum import DBItem
+from storm.expr import Desc
 from storm.locals import (
     Int,
     Reference,
@@ -200,4 +201,4 @@
         comments = IStore(DSDComment).find(
             DistroSeriesDifferenceComment,
             DSDComment.distro_series_difference == self)
-        return comments.order_by(DSDComment.id)
+        return comments.order_by(Desc(DSDComment.id))

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2010-09-03 16:43:11 +0000
+++ lib/lp/registry/model/person.py	2010-09-17 01:08:50 +0000
@@ -36,7 +36,6 @@
 import subprocess
 import weakref
 
-from bzrlib.plugins.builder.recipe import RecipeParser
 import pytz
 from sqlobject import (
     BoolCol,
@@ -918,11 +917,11 @@
     @property
     def is_team(self):
         """See `IPerson`."""
-        return self.teamowner is not None
+        return self.teamownerID is not None
 
     def isTeam(self):
         """Deprecated. Use is_team instead."""
-        return self.teamowner is not None
+        return self.is_team
 
     @property
     def mailing_list(self):
@@ -1240,18 +1239,29 @@
         if isinstance(team, (str, unicode)):
             team = PersonSet().getByName(team)
 
-        if self._inTeam_cache is None: # Initialize cache
+        if self.id == team.id:
+            # A team is always a member of itself.
+            return True
+
+        if not team.is_team:
+            # It is possible that this team is really a user since teams
+            # are users are often interchangable.
+            return False
+
+        if self._inTeam_cache is None:
+            # Initialize cache
             self._inTeam_cache = {}
         else:
+            # Retun from cache or fall through.
             try:
-                return self._inTeam_cache[team.id] # Return from cache
+                return self._inTeam_cache[team.id]
             except KeyError:
-                pass # Or fall through
+                pass 
 
         tp = TeamParticipation.selectOneBy(team=team, person=self)
         if tp is not None or self.id == team.teamownerID:
             in_team = True
-        elif team.is_team and not team.teamowner.inTeam(team):
+        elif not team.teamowner.inTeam(team):
             # The owner is not a member but must retain his rights over
             # this team. This person may be a member of the owner, and in this
             # case it'll also have rights over this team.
@@ -1613,13 +1623,16 @@
                 person_table.accountID == account_table.id,
                 account_table.status == AccountStatus.ACTIVE)))
         columns.append(account_table)
+
         def handleemail(person, column):
             #-- preferred email caching
             if not person:
                 return
             email = column
             IPropertyCache(person).preferredemail = email
+
         decorators.append(handleemail)
+
         def handleaccount(person, column):
             #-- validity caching
             if not person:
@@ -2634,9 +2647,8 @@
                      registrant, daily_build_archive=None, build_daily=False):
         """See `IPerson`."""
         from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
-        builder_recipe = RecipeParser(recipe_text).parse()
         recipe = SourcePackageRecipe.new(
-            registrant, self, name, builder_recipe, description, distroseries,
+            registrant, self, name, recipe_text, description, distroseries,
             daily_build_archive, build_daily)
         Store.of(recipe).flush()
         return recipe

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2010-08-24 19:06:33 +0000
+++ lib/lp/registry/model/projectgroup.py	2010-09-17 01:08:50 +0000
@@ -202,6 +202,9 @@
 
     def translatables(self):
         """See `IProjectGroup`."""
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this sql needs to be updated to
+        # check for the translations_usage, not official_rosetta.
         return Product.select('''
             Product.project = %s AND
             Product.official_rosetta = TRUE AND
@@ -271,7 +274,7 @@
 
         # filter based on completion. see the implementation of
         # Specification.is_complete() for more details
-        completeness =  Specification.completeness_clause
+        completeness = Specification.completeness_clause
 
         if SpecificationFilter.COMPLETE in filter:
             query += ' AND ( %s ) ' % completeness

=== modified file 'lib/lp/registry/stories/mailinglists/moderation.txt'
--- lib/lp/registry/stories/mailinglists/moderation.txt	2009-12-03 20:28:54 +0000
+++ lib/lp/registry/stories/mailinglists/moderation.txt	2010-09-17 01:08:50 +0000
@@ -100,6 +100,21 @@
     2 messages have been posted to your mailing list...
     ...
 
+If there are more messages than the batch size, they get batched.
+
+    >>> admin_browser.open(
+    ...     'http://launchpad.dev/~guadamen/+mailinglist-moderate?batch=1')
+    >>> find_tag_by_id(admin_browser.contents, 'upper-batch-nav-batchnav-next')['class']
+    u'next'
+    >>> find_tag_by_id(admin_browser.contents, 'lower-batch-nav-batchnav-next')['class']
+    u'next'
+
+To test easily, we use the default batch size below.
+
+    >>> admin_browser.open(
+    ...     'http://launchpad.dev/~guadamen/+mailinglist-moderate')
+
+
 Each held message displays some details about what's being held.
 
     >>> print extract_text(find_tag_by_id(

=== modified file 'lib/lp/registry/stories/person/xx-person-edit.txt'
--- lib/lp/registry/stories/person/xx-person-edit.txt	2010-09-13 21:16:30 +0000
+++ lib/lp/registry/stories/person/xx-person-edit.txt	2010-09-17 01:08:50 +0000
@@ -4,7 +4,8 @@
 A user wants to check if his personal information is up to date in
 Launchpad.
 
-    >>> login(ANONYMOUS)
+    >>> from lp.testing.sampledata import ADMIN_EMAIL
+    >>> login(ADMIN_EMAIL)
     >>> user = factory.makePerson(name='ray', displayname='Ray Ray',
     ...                           email='ray@xxxxxxxxxxx',
     ...                           password='test')

=== modified file 'lib/lp/registry/templates/distribution-index.pt'
--- lib/lp/registry/templates/distribution-index.pt	2010-08-24 20:11:37 +0000
+++ lib/lp/registry/templates/distribution-index.pt	2010-09-17 01:08:50 +0000
@@ -52,7 +52,7 @@
       <div class="yui-g">
         <div class="yui-u first">
           <div tal:replace="structure context/@@+portlet-listfaqs"
-            tal:condition="context/official_answers" />
+            tal:condition="context/answers_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:replace="structure context/@@+portlet-latestbugs"
             tal:condition="context/bug_tracking_usage/enumvalue:LAUNCHPAD" />
@@ -62,10 +62,10 @@
 
         <div class="yui-u">
           <div tal:replace="structure context/@@+portlet-latestquestions"
-            tal:condition="context/official_answers" />
+            tal:condition="context/answers_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:replace="structure context/@@+portlet-latestspecs"
-            tal:condition="context/official_blueprints" />
+            tal:condition="context/blueprints_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:replace="structure context/@@+portlet-coming-sprints" />
         </div>

=== modified file 'lib/lp/registry/templates/distribution-search.pt'
--- lib/lp/registry/templates/distribution-search.pt	2010-08-24 20:11:37 +0000
+++ lib/lp/registry/templates/distribution-search.pt	2010-09-17 01:08:50 +0000
@@ -77,7 +77,7 @@
                     </a>
                     <a
                       tal:define="link package/menu:answers/new"
-                      tal:condition="distribution/official_answers"
+                      tal:condition="distribution/answers_usage/enumvalue:LAUNCHPAD"
                       tal:attributes="href link/url">
                     <img
                       tal:attributes="alt link/text"
@@ -137,7 +137,7 @@
                       condition="binary_names"
                       content="string:(Matching binaries: ${binary_names}.)">
                       (Matching binaries: bin-one, bin-two.)
-                    </tal:matching-binaries> 
+                    </tal:matching-binaries>
                   </tal:binary-search-only>
                   </p>
                 </div>

=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
--- lib/lp/registry/templates/distroseries-index.pt	2010-08-25 16:18:24 +0000
+++ lib/lp/registry/templates/distroseries-index.pt	2010-09-17 01:08:50 +0000
@@ -83,7 +83,8 @@
 
           <div
             tal:content="structure context/@@+portlet-latestspecs"
-            tal:condition="context/@@+get-involved/official_blueprints" />
+            tal:define="blueprints_usage context/@@+get-involved/blueprints_usage"
+            condition="blueprints_usage/enumvalue:LAUNCHPAD" />
 
           <div
            tal:replace="structure context/distribution/@@+portlet-coming-sprints" />

=== added file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2010-09-17 01:08:50 +0000
@@ -0,0 +1,86 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad">
+  <body>
+    <tal:heading metal:fill-slot="heading">
+      <h1 tal:content="view/label">Package differences between ...</h1>
+    </tal:heading>
+
+    <div class="top-portlet" metal:fill-slot="main"
+      tal:define="differences view/cached_differences;
+                  series_name context/displayname;
+                  parent_name context/parent_series/displayname;">
+      <p>Source packages shown here are present in both
+         <tal:replace replace="series_name">Derilucid</tal:replace>
+         and the parent series,
+         <tal:replace replace="context/parent_series/fullseriesname">Ubuntu Lucid
+         </tal:replace>, but are different somehow. Changes could be in
+         either or both series so check the versions (and the diff if
+         necessary) before syncing the
+         <tal:replace replace="parent_name">Lucid
+         </tal:replace> version
+         (<a href="/+help/soyuz/derived-series-syncing.html" target="help">Read
+             more about syncing from the parent series</a>).
+      </p>
+
+      <div tal:condition="differences/batch">
+        <tal:navigation_top
+          replace="structure differences/@@+navigation-links-upper" />
+          <table class="listing">
+          <thead>
+            <tr>
+              <th>Source</th>
+              <th><tal:replace replace="parent_name" /> version</th>
+              <th><tal:replace replace="series_name" /> version</th>
+              <th>Last uploaded</th>
+              <th>Latest comment</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tal:difference repeat="difference differences/batch">
+            <tr tal:define="parent_source_pub difference/parent_source_pub;
+                            source_pub difference/source_pub;
+                            signer source_pub/sourcepackagerelease/uploader/fmt:link|nothing;">
+              <td><span
+                      tal:replace="parent_source_pub/source_package_name">Foo</span>
+              </td>
+              <td><a tal:attributes="href parent_source_pub/sourcepackagerelease/fmt:url">
+                    <tal:replace
+                    replace="parent_source_pub/sourcepackagerelease/version"/></a>
+              </td>
+              <td><a tal:attributes="href source_pub/sourcepackagerelease/fmt:url">
+                    <tal:replace
+                    replace="source_pub/sourcepackagerelease/version"/></a>
+              </td>
+              <td>
+                  <span tal:attributes="title difference/source_pub/datepublished/fmt:datetime"
+                        tal:content="difference/source_pub/datepublished/fmt:approximatedate">2005-09-16</span>
+                  <tal:signer condition="signer">
+                   by <a tal:replace="structure signer">Steph Smith</a>
+                  </tal:signer>
+              </td>
+              <td>
+                <tal:comment tal:define="comment python:difference.getComments().first();"
+                             tal:condition="comment">
+                  <span tal:replace="comment/comment/fmt:shorten/50">I'm on this.</span>
+                  <br /><span class="greyed-out greylink"><span
+                      tal:replace="comment/message/datecreated/fmt:approximatedate">2005-09-16</span>
+                  by <a tal:replace="structure
+                      comment/message/owner/fmt:link">joesmith</a></span>
+                </tal:comment>
+              </td>
+            </tr>
+
+            </tal:difference>
+          </tbody>
+        </table>
+      </div>
+
+    </div>
+
+  </body>
+</html>

=== modified file 'lib/lp/registry/templates/product-index.pt'
--- lib/lp/registry/templates/product-index.pt	2010-08-26 17:25:46 +0000
+++ lib/lp/registry/templates/product-index.pt	2010-09-17 01:08:50 +0000
@@ -215,7 +215,7 @@
       <div class="yui-g">
         <div class="yui-u first">
           <div tal:content="structure context/@@+portlet-listfaqs"
-            tal:condition="context/official_answers" />
+            tal:condition="context/answers_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:content="structure context/@@+portlet-latestbugs"
             tal:condition="context/bug_tracking_usage/enumvalue:LAUNCHPAD" />
@@ -227,10 +227,10 @@
 
         <div class="yui-u">
           <div tal:content="structure context/@@+portlet-latestquestions"
-            tal:condition="context/official_answers" />
+            tal:condition="context/answers_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:content="structure context/@@+portlet-latestspecs"
-            tal:condition="context/official_blueprints" />
+            tal:condition="context/blueprints_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:content="structure context/@@+portlet-coming-sprints" />
         </div>

=== modified file 'lib/lp/registry/templates/productseries-index.pt'
--- lib/lp/registry/templates/productseries-index.pt	2010-08-25 15:24:48 +0000
+++ lib/lp/registry/templates/productseries-index.pt	2010-09-17 01:08:50 +0000
@@ -145,7 +145,7 @@
         <div class="yui-u">
           <div
             tal:content="structure context/@@+portlet-latestspecs"
-            tal:condition="context/@@+get-involved/official_blueprints" />
+            tal:condition="context/@@+get-involved/blueprints_usage/enumvalue:LAUNCHPAD" />
 
           <div tal:replace="structure context/product/@@+portlet-coming-sprints" />
 

=== modified file 'lib/lp/registry/templates/project-index.pt'
--- lib/lp/registry/templates/project-index.pt	2010-08-25 15:37:37 +0000
+++ lib/lp/registry/templates/project-index.pt	2010-09-17 01:08:50 +0000
@@ -72,11 +72,11 @@
         <tal:details replace="structure context/@@+details" />
 
         <tal:faqs content="structure context/@@+portlet-listfaqs"
-          condition="context/@@+get-involved/official_answers" />
+          condition="context/@@+get-involved/answers_usage/enumvalue:LAUNCHPAD" />
 
         <tal:has-many-project condition="view/has_many_projects">
           <tal:questions content="structure context/@@+portlet-latestquestions"
-            condition="context/@@+get-involved/official_answers" />
+            condition="context/@@+get-involved/answers_usage/enumvalue:LAUNCHPAD" />
         </tal:has-many-project>
 
         <tal:bugs content="structure context/@@+portlet-latestbugs"
@@ -84,7 +84,7 @@
 
         <tal:has-many-project condition="view/has_many_projects">
           <tal:specs content="structure context/@@+portlet-latestspecs"
-            condition="context/@@+get-involved/official_blueprints" />
+            condition="context/@@+get-involved/blueprints_usage/enumvalue:LAUNCHPAD" />
         </tal:has-many-project>
 
         <tal:contributors content="structure context/@@+portlet-top-contributors"/>
@@ -119,10 +119,10 @@
 
         <tal:has-few-project condition="not: view/has_many_projects">
           <tal:questions content="structure context/@@+portlet-latestquestions"
-            condition="context/@@+get-involved/official_answers" />
+            condition="context/@@+get-involved/answers_usage/enumvalue:LAUNCHPAD" />
 
           <tal:specs content="structure context/@@+portlet-latestspecs"
-            condition="context/@@+get-involved/official_blueprints" />
+            condition="context/@@+get-involved/blueprints_usage/enumvalue:LAUNCHPAD" />
 
           <tal:sprints content="structure context/@@+portlet-coming-sprints" />
         </tal:has-few-project>

=== modified file 'lib/lp/registry/templates/team-mailinglist-moderate.pt'
--- lib/lp/registry/templates/team-mailinglist-moderate.pt	2009-09-14 01:28:41 +0000
+++ lib/lp/registry/templates/team-mailinglist-moderate.pt	2010-09-17 01:08:50 +0000
@@ -35,16 +35,24 @@
           action for obvious spam.</li>
           <li><strong>Hold</strong> - Continue to hold the message, deferring
           your decision until later.</li>
+          <li>Toss</li>
         </ul>
       </div>
-      <table class="listing" metal:fill-slot="widgets">
+      <div metal:fill-slot="widgets">
+      <tal:navigation
+        replace="structure view/held_messages/@@+navigation-links-upper" />
+
+      <table class="listing">
         <thead><tr>
           <th>Message details</th>
           <th>Approve</th><th>Decline</th><th>Discard</th><th>Hold</th>
         </tr></thead>
-        <span tal:repeat="message view/held_messages"
+        <span tal:repeat="message view/held_messages/currentBatch"
              tal:content="structure message/@@+moderation" />
       </table>
+      <tal:navigation
+        replace="structure view/held_messages/@@+navigation-links-lower" />
+      </div>
     </metal:form>
   </div>
   <span tal:condition="not: view/hold_count" id="legend">

=== modified file 'lib/lp/registry/tests/test_distroseriesdifference.py'
--- lib/lp/registry/tests/test_distroseriesdifference.py	2010-09-01 12:23:01 +0000
+++ lib/lp/registry/tests/test_distroseriesdifference.py	2010-09-17 01:08:50 +0000
@@ -254,7 +254,8 @@
         self.assertEqual(ds_diff, dsd_comment.distro_series_difference)
 
     def test_getComments(self):
-        # All comments for this difference are returned.
+        # All comments for this difference are returned with the
+        # most recent comment first.
         ds_diff = self.factory.makeDistroSeriesDifference()
 
         with person_logged_in(ds_diff.owner):
@@ -264,7 +265,7 @@
                 ds_diff.owner, "Wait until version 2.1")
 
         self.assertEqual(
-            [dsd_comment, dsd_comment_2], list(ds_diff.getComments()))
+            [dsd_comment_2, dsd_comment], list(ds_diff.getComments()))
 
     def test_addComment_not_public(self):
         # Comments cannot be added with launchpad.View.

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2010-09-03 08:55:48 +0000
+++ lib/lp/registry/tests/test_person.py	2010-09-17 01:08:50 +0000
@@ -11,6 +11,7 @@
 import pytz
 from testtools.matchers import LessThan
 import transaction
+from storm.store import Store
 from zope.component import getUtility
 from zope.interface import providedBy
 from zope.security.proxy import removeSecurityProxy
@@ -123,7 +124,7 @@
             in memberships]
         self.assertEqual(expected_memberships, memberships)
 
-    def test_getPathsToTeamsComplicated(self):
+    def test_getPathsToTeams_complicated(self):
         d_team = self.factory.makeTeam(name='d', owner=self.b_team)
         e_team = self.factory.makeTeam(name='e')
         f_team = self.factory.makeTeam(name='f', owner=e_team)
@@ -153,7 +154,7 @@
             in memberships]
         self.assertEqual(expected_memberships, memberships)
 
-    def test_getPathsToTeamsMultiplePaths(self):
+    def test_getPathsToTeams_multiple_paths(self):
         d_team = self.factory.makeTeam(name='d', owner=self.b_team)
         login_person(self.a_team.teamowner)
         self.c_team.addMember(d_team, self.c_team.teamowner)
@@ -175,6 +176,49 @@
             in memberships]
         self.assertEqual(expected_memberships, memberships)
 
+    def test_inTeam_direct_team(self):
+        # Verify direct membeship is True and the cache is populated.
+        self.assertTrue(self.user.inTeam(self.a_team))
+        self.assertEqual(
+            {self.a_team.id: True},
+            removeSecurityProxy(self.user)._inTeam_cache)
+
+    def test_inTeam_indirect_team(self):
+        # Verify indirect membeship is True and the cache is populated.
+        self.assertTrue(self.user.inTeam(self.b_team))
+        self.assertEqual(
+            {self.b_team.id: True},
+            removeSecurityProxy(self.user)._inTeam_cache)
+
+    def test_inTeam_cache_cleared_by_membership_change(self):
+        # Verify a change in membership clears the team cache.
+        self.user.inTeam(self.a_team)
+        with person_logged_in(self.b_team.teamowner):
+            self.b_team.addMember(self.user, self.b_team.teamowner)
+        self.assertEqual(
+            {},
+            removeSecurityProxy(self.user)._inTeam_cache)
+
+    def test_inTeam_person_is_false(self):
+        # Verify a user cannot be a member of another user.
+        other_user = self.factory.makePerson()
+        self.assertFalse(self.user.inTeam(other_user))
+
+    def test_inTeam_person_does_not_build_TeamParticipation_cache(self):
+        # Verify when a user is the argument, a DB call to TeamParticipation
+        # was not made to learn this.
+        other_user = self.factory.makePerson()
+        Store.of(self.user).invalidate()
+        # Load the two person objects only by reading a non-id attribute
+        # unrelated to team/person or teamparticipation.
+        other_user.name
+        self.user.name
+        self.assertFalse(
+            self.assertStatementCount(0, self.user.inTeam, other_user))
+        self.assertEqual(
+            {},
+            removeSecurityProxy(self.user)._inTeam_cache)
+
 
 class TestPerson(TestCaseWithFactory):
 

=== modified file 'lib/lp/services/features/__init__.py'
--- lib/lp/services/features/__init__.py	2010-08-05 23:40:32 +0000
+++ lib/lp/services/features/__init__.py	2010-09-17 01:08:50 +0000
@@ -26,4 +26,9 @@
 
 def getFeatureFlag(flag):
     """Get the value of a flag for this thread's scopes."""
-    return per_thread.features.getFlag(flag)
+    # Workaround for bug 631884 - features have two homes, threads and
+    # requests.
+    features = getattr(per_thread, 'features', None)
+    if features is None:
+        return None
+    return features.getFlag(flag)

=== modified file 'lib/lp/services/features/tests/test_flags.py'
--- lib/lp/services/features/tests/test_flags.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/features/tests/test_flags.py	2010-09-17 01:08:50 +0000
@@ -129,6 +129,12 @@
         finally:
             per_thread.features = None
 
+    def test_threadGetFlagNoContext(self):
+        # If there is no context, please don't crash. workaround for the root
+        # cause in bug 631884.
+        per_thread.features = None
+        self.assertEqual(None, getFeatureFlag('ui.icing'))
+
     def testLazyScopeLookup(self):
         # feature scopes may be a bit expensive to look up, so we do it only
         # when it will make a difference to the result.

=== added file 'lib/lp/services/features/tests/test_webapp.py'
--- lib/lp/services/features/tests/test_webapp.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/features/tests/test_webapp.py	2010-09-17 01:08:50 +0000
@@ -0,0 +1,41 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for webapp glue."""
+
+__metaclass__ = type
+
+from canonical.testing import layers
+from lp.services.features import webapp
+from lp.testing import TestCase
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+
+
+class TestScopesFromRequest(TestCase):
+
+    def test_pageid_scope_normal(self):
+        request = LaunchpadTestRequest()
+        request.setInWSGIEnvironment('launchpad.pageid', 'foo:bar')
+        scopes = webapp.ScopesFromRequest(request)
+        self.assertTrue(scopes.lookup('pageid:'))
+        self.assertTrue(scopes.lookup('pageid:foo'))
+        self.assertTrue(scopes.lookup('pageid:foo:bar'))
+        self.assertFalse(scopes.lookup('pageid:foo:bar#quux'))
+
+    def test_pageid_scope_collection(self):
+        request = LaunchpadTestRequest()
+        request.setInWSGIEnvironment('launchpad.pageid', 'scoped:thing:#type')
+        scopes = webapp.ScopesFromRequest(request)
+        self.assertTrue(scopes.lookup('pageid:'))
+        self.assertTrue(scopes.lookup('pageid:scoped'))
+        self.assertTrue(scopes.lookup('pageid:scoped:thing'))
+        self.assertTrue(scopes.lookup('pageid:scoped:thing:#type'))
+        self.assertFalse(scopes.lookup('pageid:scoped:thing:#type:other'))
+
+    def test_pageid_scope_empty(self):
+        request = LaunchpadTestRequest()
+        request.setInWSGIEnvironment('launchpad.pageid', '')
+        scopes = webapp.ScopesFromRequest(request)
+        self.assertTrue(scopes.lookup('pageid:'))
+        self.assertFalse(scopes.lookup('pageid:foo'))
+        self.assertFalse(scopes.lookup('pageid:foo'))

=== modified file 'lib/lp/services/features/webapp.py'
--- lib/lp/services/features/webapp.py	2010-09-06 20:25:28 +0000
+++ lib/lp/services/features/webapp.py	2010-09-17 01:08:50 +0000
@@ -10,6 +10,7 @@
 import canonical.config
 from lp.services.features import per_thread
 from lp.services.features.flags import FeatureController
+from lp.services.propertycache import cachedproperty
 
 
 class ScopesFromRequest(object):
@@ -24,14 +25,57 @@
         Currently supports the following scopes:
          - default
          - is_edge/is_lpnet etc (thunks through to the config)
+         - pageid:
+           This scope works on a namespace model: for a page
+           with pageid SomeType:+view#subselector
+           The following page ids scopes will match:
+             - pageid:   (but use 'default' as it is simpler)
+             - pageid:SomeType
+             - pageid:SomeType:+view
+             - pageid:SomeType:+view#subselector
         """
         if scope_name == 'default':
             return True
+        if scope_name.startswith('pageid:'):
+            return self._lookup_pageid(scope_name[len('pageid:'):])
         parts = scope_name.split('.')
         if len(parts) == 2:
             if parts[0] == 'server':
                 return canonical.config.config['launchpad']['is_' + parts[1]]
 
+    def _lookup_pageid(self, pageid_scope):
+        """Lookup a pageid as a scope.
+
+        pageid scopes are written as 'pageid:' + the pageid to match.
+        Page ids are treated as a namespace with : and # delimiters.
+
+        E.g. the scope 'pageid:Foo' will affect pages with pageids:
+        Foo
+        Foo:Bar
+        Foo#quux
+        """
+        scope_segments = self._pageid_to_namespace(pageid_scope)
+        request_segments = self._request_pageid_namespace
+        # In 2.6, this can be replaced with izip_longest
+        for pos, name in enumerate(scope_segments):
+            if pos == len(request_segments):
+                return False
+            if request_segments[pos] != name:
+                return False
+        return True
+
+    def _pageid_to_namespace(self, pageid):
+        """Return a list of namespace elements for pageid."""
+        # Normalise delimiters.
+        pageid = pageid.replace('#', ':')
+        # Create a list to walk, empty namespaces are elided.
+        return [name for name in pageid.split(':') if name]
+
+    @cachedproperty
+    def _request_pageid_namespace(self):
+        return tuple(self._pageid_to_namespace(
+            self._request._orig_env.get('launchpad.pageid', '')))
+
 
 def start_request(event):
     """Register FeatureController."""

=== modified file 'lib/lp/services/memcache/client.py'
--- lib/lp/services/memcache/client.py	2010-09-04 05:59:16 +0000
+++ lib/lp/services/memcache/client.py	2010-09-17 01:08:50 +0000
@@ -6,12 +6,14 @@
 __metaclass__ = type
 __all__ = []
 
+import logging
 import re
 
 from lazr.restful.utils import get_current_browser_request
 import memcache
 
 from canonical.config import config
+from lp.services import features
 from lp.services.timeline.requesttimeline import get_request_timeline
 
 
@@ -32,7 +34,13 @@
         timeline = get_request_timeline(request)
         return timeline.start("memcache-%s" % suffix, key)
 
+    @property
+    def _enabled(self):
+        return features.getFeatureFlag('memcache') != 'disabled'
+
     def get(self, key):
+        if not self._enabled:
+            return None
         action = self.__get_timeline_action("get", key)
         try:
             return memcache.Client.get(self, key)
@@ -40,9 +48,16 @@
             action.finish()
 
     def set(self, key, value, time=0, min_compress_len=0):
+        if not self._enabled:
+            return None
         action = self.__get_timeline_action("set", key)
         try:
-            return memcache.Client.set(self, key, value, time=time,
+            success = memcache.Client.set(self, key, value, time=time,
                 min_compress_len=min_compress_len)
+            if success:
+                logging.debug("Memcache set succeeded for %s", key)
+            else:
+                logging.warn("Memcache set failed for %s", key)
+            return success
         finally:
             action.finish()

=== modified file 'lib/lp/services/memcache/doc/tales-cache.txt'
--- lib/lp/services/memcache/doc/tales-cache.txt	2010-06-24 17:59:02 +0000
+++ lib/lp/services/memcache/doc/tales-cache.txt	2010-09-17 01:08:50 +0000
@@ -343,3 +343,36 @@
 
     >>> ignore = config.pop('is_production')
 
+Disabling
+---------
+
+Memcache in templates can be disabled entirely by setting the memcache flag to
+'disabled'.
+
+    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+    >>> from lp.services.features.model import FeatureFlag, getFeatureStore
+    >>> from lp.services.features.webapp import ScopesFromRequest
+    >>> from lp.services.features.flags import FeatureController
+    >>> from lp.services.features import per_thread
+    >>> ignore = getFeatureStore().add(FeatureFlag(
+    ...     scope=u'default', flag=u'memcache', value=u'disabled',
+    ...     priority=1))
+    >>> empty_request = LaunchpadTestRequest()
+    >>> per_thread.features = FeatureController(
+    ...     ScopesFromRequest(empty_request).lookup)
+
+And now what cached before will not cache.
+
+    >>> template = TestPageTemplate(dedent("""\
+    ...     <div tal:content="cache:public">
+    ...         <span tal:content="param">placeholder</span>
+    ...     </div>"""))
+
+    >>> print template(param='first')
+    <div>
+        <span>first</span>
+    </div>
+    >>> print template(param='second')
+    <div>
+        <span>second</span>
+    </div>

=== modified file 'lib/lp/services/memcache/tales.py'
--- lib/lp/services/memcache/tales.py	2010-08-20 20:31:18 +0000
+++ lib/lp/services/memcache/tales.py	2010-09-17 01:08:50 +0000
@@ -148,7 +148,9 @@
         + ''.join(chr(i) for i in range(ord(':')+1, 127)) + '_' * 129)
 
     # We strip digits from our LPCONFIG when generating the key
-    # to ensure that edge1 and edge4 share cache.
+    # to ensure that multiple appserver instances sharing a memcache instance
+    # can get hits from each other. For instance edge1 and edge4 are in this
+    # situation.
     _lpconfig = config.instance_name.rstrip('0123456789')
 
     def getKey(self, econtext):
@@ -278,11 +280,7 @@
             rule = '%s [%s seconds]' % (self._memcache_expr, self._max_age)
             value = "<!-- Cache hit: %s -->%s<!-- End cache hit: %s -->" % (
                 rule, value, rule)
-        if getUtility(IMemcacheClient).set(
-            self._key, value, self._max_age):
-            logging.debug("Memcache set succeeded for %s", self._key)
-        else:
-            logging.warn("Memcache set failed for %s", self._key)
+        getUtility(IMemcacheClient).set(self._key, value, self._max_age)
 
     def __repr__(self):
         return "<MemcacheCallback %s %d>" % (self._key, self._max_age)

=== added file 'lib/lp/soyuz/help/derived-series-syncing.html'
--- lib/lp/soyuz/help/derived-series-syncing.html	1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/help/derived-series-syncing.html	2010-09-17 01:08:50 +0000
@@ -0,0 +1,46 @@
+<html>
+  <head>
+    <title>Syncing software from a parent series</title>
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssreset/reset.css" />
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssfonts/fonts.css" />
+    <link rel="stylesheet" type="text/css"
+          href="/+icing/yui/cssbase/base.css" />
+  </head>
+  <body>
+    <h1>Syncing software from a parent series</h1>
+
+    <p>
+      A nice introduction text written by the great
+    </p>
+
+    <p>
+      <strong>Important:</strong> Syncing a package from the parent
+      series will overwrite ....
+    </p>
+
+    <h2>Checking the differences</h2>
+
+    <p>
+    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
+    eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
+    ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+    aliquip ex ea commodo consequat. Duis aute irure dolor in
+    reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
+    pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+    culpa qui officia deserunt mollit anim id est laborum.
+    </p>
+
+    <p>
+    <strong>Step 1:</strong>
+    Sed ut perspiciatis unde omnis iste natus error sit voluptatem
+    accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
+    quae ab illo inventore veritatis et quasi architecto beatae vitae
+    dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit
+    aspernatur aut odit aut fugit, sed quia consequuntur magni dolores
+    eos qui ratione voluptatem sequi nesciunt.
+    </p>
+
+  </body>
+</html>

=== modified file 'lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt'
--- lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt	2010-08-30 02:07:38 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-buildfarm-index.txt	2010-09-17 01:08:50 +0000
@@ -144,12 +144,12 @@
     >>> admin_browser.getControl(name='field.processor').displayValue
     ['Intel 386']
 
-By default, builders are created as 'active', although the
+By default, builders are created as 'Publicly Visible', although the
 administrator can change this value during creation time.
 
-    >>> print admin_browser.getControl('Active').selected
+    >>> print admin_browser.getControl('Publicly Visible').selected
     True
-    >>> admin_browser.getControl('Active').selected = False
+    >>> admin_browser.getControl('Publicly Visible').selected = False
 
 Builder as created as 'non-virtual' by default, but the administrator
 can also modify that while creating a builder.

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-09-03 16:43:11 +0000
+++ lib/lp/testing/factory.py	2010-09-17 01:08:50 +0000
@@ -93,6 +93,7 @@
     IStoreSelector,
     MAIN_STORE,
     )
+from lp.app.enums import ServiceUsage
 from lp.archiveuploader.dscfile import DSCFile
 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
 from lp.blueprints.interfaces.specification import (
@@ -838,7 +839,7 @@
         self, name=None, project=None, displayname=None,
         licenses=None, owner=None, registrant=None,
         title=None, summary=None, official_malone=None,
-        official_rosetta=None, bug_supervisor=None):
+        translations_usage=None, bug_supervisor=None):
         """Create and return a new, arbitrary Product."""
         if owner is None:
             owner = self.makePerson()
@@ -867,8 +868,9 @@
             registrant=registrant)
         if official_malone is not None:
             removeSecurityProxy(product).official_malone = official_malone
-        if official_rosetta is not None:
-            removeSecurityProxy(product).official_rosetta = official_rosetta
+        if translations_usage is not None:
+            naked_product = removeSecurityProxy(product)
+            naked_product.translations_usage = translations_usage
         if bug_supervisor is not None:
             naked_product = removeSecurityProxy(product)
             naked_product.bug_supervisor = bug_supervisor
@@ -1873,7 +1875,8 @@
             source_pub = self.makeSourcePackagePublishingHistory(
                 distroseries=derived_series,
                 version=versions.get('derived'),
-                sourcepackagename=source_package_name)
+                sourcepackagename=source_package_name,
+                status = PackagePublishingStatus.PUBLISHED)
 
         if difference_type is not (
             DistroSeriesDifferenceType.UNIQUE_TO_DERIVED_SERIES):
@@ -1881,7 +1884,8 @@
             source_pub = self.makeSourcePackagePublishingHistory(
                 distroseries=derived_series.parent_series,
                 version=versions.get('parent'),
-                sourcepackagename=source_package_name)
+                sourcepackagename=source_package_name,
+                status = PackagePublishingStatus.PUBLISHED)
 
         return getUtility(IDistroSeriesDifferenceSource).new(
             derived_series, source_package_name, difference_type,
@@ -2035,7 +2039,7 @@
                                 distroseries=None, name=None,
                                 description=None, branches=(),
                                 build_daily=False, daily_build_archive=None,
-                                is_stale=None):
+                                is_stale=None, recipe=None):
         """Make a `SourcePackageRecipe`."""
         if registrant is None:
             registrant = self.makePerson()
@@ -2051,7 +2055,10 @@
         if daily_build_archive is None:
             daily_build_archive = self.makeArchive(
                 distribution=distroseries.distribution, owner=owner)
-        recipe = self.makeRecipe(*branches)
+        if recipe is None:
+            recipe = self.makeRecipeText(*branches)
+        else:
+            assert branches == ()
         source_package_recipe = getUtility(ISourcePackageRecipeSource).new(
             registrant, owner, name, recipe, description, [distroseries],
             daily_build_archive, build_daily)
@@ -2173,7 +2180,8 @@
             productseries = self.makeProductSeries(owner=owner)
             # Make it use Translations, otherwise there's little point
             # to us creating a template for it.
-            removeSecurityProxy(productseries).product.official_rosetta = True
+            naked_series = removeSecurityProxy(productseries)
+            naked_series.product.translations_usage = ServiceUsage.LAUNCHPAD
         templateset = getUtility(IPOTemplateSet)
         subset = templateset.getSubset(
             distroseries, sourcepackagename, productseries)

=== modified file 'lib/lp/testing/fakelibrarian.py'
--- lib/lp/testing/fakelibrarian.py	2010-09-05 10:49:21 +0000
+++ lib/lp/testing/fakelibrarian.py	2010-09-17 01:08:50 +0000
@@ -152,8 +152,13 @@
 
     def getURLForAlias(self, aliasID, secure=False):
         """See `IFileDownloadClient`."""
-        alias = self.aliases.get(aliasID)
-        path = get_libraryfilealias_download_path(aliasID, alias.filename)
+        return self.getURLForAliasObject(self.aliases.get(int(aliasID)))
+
+    def getURLForAliasObject(self, alias):
+        """See `IFileDownloadClient`."""
+        if alias.deleted:
+            return None
+        path = get_libraryfilealias_download_path(alias.id, alias.filename)
         return urljoin(self.download_url, path)
 
     def getFileByAlias(self, aliasID,

=== modified file 'lib/lp/testing/menu.py'
--- lib/lp/testing/menu.py	2010-08-23 04:07:24 +0000
+++ lib/lp/testing/menu.py	2010-09-17 01:08:50 +0000
@@ -22,6 +22,6 @@
             view_name = None
         try:
             canonical_url(context, view_name=view_name, rootsite=link.site)
-        except:
+        except Exception:
             return 'Bad link %s: %s' % (link.name, canonical_url(context))
     return True

=== modified file 'lib/lp/testing/tests/test_fakelibrarian.py'
--- lib/lp/testing/tests/test_fakelibrarian.py	2010-09-03 13:08:42 +0000
+++ lib/lp/testing/tests/test_fakelibrarian.py	2010-09-17 01:08:50 +0000
@@ -10,6 +10,7 @@
 import transaction
 from transaction.interfaces import ISynchronizer
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.database.librarian import (
     LibraryFileAlias,
@@ -42,9 +43,9 @@
         """
         name = self.factory.getUniqueString() + '.txt'
         text = self.factory.getUniqueString()
-        alias_id = getUtility(ILibrarianClient).addFile(
+        alias = getUtility(ILibraryFileAliasSet).create(
             name, len(text), StringIO(text), 'text/plain')
-        return name, text, alias_id
+        return name, text, alias
 
     def test_baseline(self):
         self.assertTrue(
@@ -55,33 +56,28 @@
                 ILibraryFileAliasSet, getUtility(ILibraryFileAliasSet)))
 
     def test_insert_retrieve(self):
-        name, text, alias_id = self._storeFile()
-        self.assertIsInstance(alias_id, (int, long))
+        name, text, alias = self._storeFile()
+        self.assertIsInstance(alias.id, (int, long))
 
         transaction.commit()
 
-        library_file = getUtility(ILibrarianClient).getFileByAlias(alias_id)
+        library_file = getUtility(ILibrarianClient).getFileByAlias(alias.id)
         self.assertEqual(text, library_file.read())
 
     def test_alias_set(self):
-        name, text, alias_id = self._storeFile()
-
-        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias_id]
-
-        self.assertEqual(alias_id, retrieved_alias.id)
-        self.assertEqual(name, retrieved_alias.filename)
+        name, text, alias = self._storeFile()
+        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias.id]
+        self.assertEqual(alias, retrieved_alias)
 
     def test_read(self):
-        name, text, alias_id = self._storeFile()
+        name, text, alias = self._storeFile()
         transaction.commit()
-
-        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias_id]
-        retrieved_alias.open()
-        self.assertEqual(text, retrieved_alias.read())
+        alias.open()
+        self.assertEqual(text, alias.read())
 
     def test_uncommitted_file(self):
-        name, text, alias_id = self._storeFile()
-        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias_id]
+        name, text, alias = self._storeFile()
+        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias.id]
         self.assertRaises(LookupError, retrieved_alias.open)
 
     def test_incorrect_upload_size(self):
@@ -95,9 +91,17 @@
 
     def test_create_returns_alias(self):
         alias = getUtility(ILibraryFileAliasSet).create(
-            'foo.txt', 3, StringIO('foo'), 'text/plain', debugID='txt')
+            'foo.txt', 3, StringIO('foo'), 'text/plain')
         self.assertIsInstance(alias, LibraryFileAlias)
 
+    def test_addFile_returns_alias_id(self):
+        alias_id = getUtility(ILibrarianClient).addFile(
+            'bar.txt', 3, StringIO('bar'), 'text/plain')
+        self.assertIsInstance(alias_id, (int, long))
+        self.assertIsInstance(
+            getUtility(ILibraryFileAliasSet)[alias_id],
+            LibraryFileAlias)
+
     def test_debugID_is_harmless(self):
         # addFile takes an argument debugID that doesn't do anything
         # observable.  We get a LibraryFileAlias regardless.
@@ -105,6 +109,32 @@
             'txt.txt', 3, StringIO('txt'), 'text/plain', debugID='txt')
         self.assertNotEqual(None, alias)
 
+    def test_getURLForAlias(self):
+        name, text, alias = self._storeFile()
+        librarian = getUtility(ILibrarianClient)
+        self.assertIn(
+            librarian.getURLForAlias(alias.id),
+            (alias.http_url, alias.https_url))
+
+    def test_getURLForAliasObject(self):
+        name, text, alias = self._storeFile()
+        librarian = getUtility(ILibrarianClient)
+        self.assertEqual(
+            librarian.getURLForAlias(alias.id),
+            librarian.getURLForAliasObject(alias))
+
+    def test_getURL(self):
+        name, text, alias = self._storeFile()
+        self.assertIn(alias.getURL(), (alias.http_url, alias.https_url))
+
+    def test_deleted_alias_has_no_url(self):
+        name, text, alias = self._storeFile()
+        librarian = getUtility(ILibrarianClient)
+
+        self.assertNotEqual(None, alias.getURL())
+        removeSecurityProxy(alias).content = None
+        self.assertIs(None, alias.getURL())
+
 
 class TestFakeLibrarian(LibraryAccessScenarioMixin, TestCaseWithFactory):
     """Test the supported interface subset on the fake librarian."""
@@ -120,11 +150,11 @@
         self.assertIsInstance(self.fake_librarian, FakeLibrarian)
 
     def test_pretend_commit(self):
-        name, text, alias_id = self._storeFile()
+        name, text, alias = self._storeFile()
 
         self.fake_librarian.pretendCommit()
 
-        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias_id]
+        retrieved_alias = getUtility(ILibraryFileAliasSet)[alias.id]
         retrieved_alias.open()
         self.assertEqual(text, retrieved_alias.read())
 

=== modified file 'lib/lp/translations/browser/distroseries.py'
--- lib/lp/translations/browser/distroseries.py	2010-08-24 10:45:57 +0000
+++ lib/lp/translations/browser/distroseries.py	2010-09-17 01:08:50 +0000
@@ -178,14 +178,16 @@
         self.request.response.addInfoNotification(
             'Your changes have been applied.')
         self.next_url = canonical_url(
-            self.context, rootsite='translations', view_name='+language-packs')
+            self.context, rootsite='translations',
+            view_name='+language-packs')
 
     @action("Request", condition=is_langpack_admin)
     def request_action(self, action, data):
         self.updateContextFromData(data)
         self._request_full_export()
         self.next_url = canonical_url(
-            self.context, rootsite='translations', view_name='+language-packs')
+            self.context, rootsite='translations',
+            view_name='+language-packs')
 
 
 class DistroSeriesTemplatesView(BaseSeriesTemplatesView):
@@ -195,6 +197,11 @@
         super(DistroSeriesTemplatesView, self).initialize(
             series=self.context, is_distroseries=True)
 
+    def constructTemplateURL(self, template):
+        """See `BaseSeriesTemplatesView`."""
+        return '+source/%s/+pots/%s' % (
+            template.sourcepackagename.name, template.name)
+
 
 class DistroSeriesView(LaunchpadView, TranslationsMixin):
 

=== modified file 'lib/lp/translations/browser/potemplate.py'
--- lib/lp/translations/browser/potemplate.py	2010-08-25 20:04:40 +0000
+++ lib/lp/translations/browser/potemplate.py	2010-09-17 01:08:50 +0000
@@ -60,7 +60,9 @@
     )
 from canonical.launchpad.webapp.launchpadform import ReturnToReferrerMixin
 from canonical.launchpad.webapp.menu import structured
+from canonical.launchpad.webapp.tales import DateTimeFormatterAPI
 from canonical.lazr.utils import smartquote
+from lp.app.enums import service_uses_launchpad
 from lp.app.errors import NotFoundError
 from lp.registry.browser.productseries import ProductSeriesFacets
 from lp.registry.browser.sourcepackage import SourcePackageFacets
@@ -794,9 +796,10 @@
             product_or_distro = potemplate.productseries.product
         else:
             product_or_distro = potemplate.distroseries.distribution
-        official_rosetta = product_or_distro.official_rosetta
+        translations_usage = product_or_distro.translations_usage
 
-        if official_rosetta and potemplate.iscurrent:
+        if (service_uses_launchpad(translations_usage) and
+           potemplate.iscurrent):
             # This template is available for translation.
             return potemplate
         elif check_permission('launchpad.Edit', potemplate):
@@ -836,6 +839,8 @@
         self.can_edit = (
             self.can_admin or check_permission('launchpad.Edit', series))
 
+        self.user_is_logged_in = (self.user is not None)
+
     def iter_templates(self):
         potemplateset = getUtility(IPOTemplateSet)
         return potemplateset.getSubset(
@@ -848,3 +853,143 @@
             return "active-template"
         else:
             return "inactive-template"
+
+    def _renderSourcePackage(self, template):
+        """Render the `SourcePackageName` for `template`."""
+        if self.is_distroseries:
+            return cgi.escape(template.sourcepackagename.name)
+        else:
+            return None
+
+    def _renderTemplateLink(self, template, url):
+        """Render a link to `template`.
+
+        :param template: The target `POTemplate`.
+        :param url: The cached URL for `template`.
+        :return: HTML for a link to `template`.
+        """
+        text = '<a href="%s">%s</a>' % (url, cgi.escape(template.name))
+        if not template.iscurrent:
+            text += ' (inactive)'
+        return text
+
+    def _renderLastUpdateDate(self, template):
+        """Render a template's "last updated" column."""
+        formatter = DateTimeFormatterAPI(template.date_last_updated)
+        full_time = formatter.datetime()
+        date = formatter.approximatedate()
+        return ''.join([
+            '<span class="sortkey">%s</span>' % full_time,
+            '<span class="lastupdate_column" title="%s">%s</span>' % (
+                full_time, date),
+            ])
+
+    def _renderAction(self, base_url, name, path, sprite, enabled):
+        """Render an action for the "actions" column.
+
+        :param base_url: The cached URL for `template`.
+        :param name: Action name for display in the UI.
+        :param path: Path suffix for the action (relative to `base_url`).
+        :param sprite: Sprite class for the action.
+        :param enabled: Show this action?  If not, return empty string.
+        :return: HTML for the contents of the "actions" column.
+        """
+        if not enabled:
+            return ''
+
+        parameters = {
+            'base_url': base_url,
+            'name': name,
+            'path': path,
+            'sprite': sprite,
+        }
+        return (
+            '<a class="sprite %(sprite)s" href="%(base_url)s/%(path)s">'
+            '%(name)s'
+            '</a>') % parameters
+
+    def _renderActionsColumn(self, template, base_url):
+        """Render a template's "actions" column."""
+        if not self.user_is_logged_in:
+            return None
+
+        actions = [
+            ('Edit', '+edit', 'edit', self.can_edit),
+            ('Upload', '+upload', 'add', self.can_edit),
+            ('Download', '+export', 'download', self.user_is_logged_in),
+            ('Administer', '+admin', 'edit', self.can_admin),
+        ]
+        links = [
+            self._renderAction(base_url, *action) for action in actions]
+        html = '<div class="template_links">\n%s</div>'
+        return html % '\n'.join(links)
+
+    def _renderField(self, column_class, content, tag='td'):
+        """Create a table field of the given class and contents.
+
+        :param column_class: CSS class for this column.
+        :param content: HTML to go into the column.  If None, the field
+            will be omitted entirely.  (To produce an empty column, pass
+            the empty string instead.)
+        :param tag: The HTML tag to surround the field in.
+        :return: HTML for the entire table field, or the empty string if
+            `content` was None.
+        """
+        if content is None:
+            return ''
+        else:
+            return '<%s class="%s">%s</%s>' % (
+                tag, column_class, content, tag)
+
+    def constructTemplateURL(self, template):
+        """Build the URL for `template`.
+
+        Since this is performance-critical, views are allowed to
+        override it with optimized implementations.
+        """
+        return canonical_url(template)
+
+    def renderTemplatesHeader(self):
+        """Render HTML for the templates table header."""
+        if self.is_distroseries:
+            sourcepackage_header = "Source package"
+        else:
+            sourcepackage_header = None
+        if self.user_is_logged_in:
+            actions_header = "Actions"
+        else:
+            actions_header = None
+        columns = [
+            ('priority_column', "Priority"),
+            ('sourcepackage_column', sourcepackage_header),
+            ('template_column', "Template name"),
+            ('length_column', "Length"),
+            ('lastupdate_column', "Updated"),
+            ('actions_column', actions_header),
+            ]
+        return '\n'.join([
+            self._renderField(css, text, tag='th')
+            for (css, text) in columns])
+
+    def renderTemplateRow(self, template):
+        """Render HTML for an entire template row."""
+        if not self.can_edit and not template.iscurrent:
+            return ""
+
+        # Cached URL for template.
+        base_url = self.constructTemplateURL(template)
+
+        fields = [
+            ('priority_column', template.priority),
+            ('sourcepackage_column', self._renderSourcePackage(template)),
+            ('template_column', self._renderTemplateLink(template, base_url)),
+            ('length_column', template.messagecount),
+            ('lastupdate_column', self._renderLastUpdateDate(template)),
+            ('actions_column', self._renderActionsColumn(template, base_url)),
+        ]
+
+        tds = [self._renderField(*field) for field in fields]
+
+        css = self.rowCSSClass(template)
+        return '<tr class="template_row %s">\n%s</tr>\n' % (
+            css, '\n'.join(tds))

=== modified file 'lib/lp/translations/browser/product.py'
--- lib/lp/translations/browser/product.py	2010-08-24 10:45:57 +0000
+++ lib/lp/translations/browser/product.py	2010-09-17 01:08:50 +0000
@@ -19,6 +19,7 @@
     )
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.menu import NavigationMenu
+from lp.app.enums import service_uses_launchpad
 from lp.registry.browser.product import ProductEditView
 from lp.registry.interfaces.product import IProduct
 from lp.registry.interfaces.productseries import IProductSeries
@@ -51,12 +52,14 @@
     def translationdownload(self):
         text = 'Download'
         preferred_series = self.context.primary_translatable
-        enabled = (self.context.official_rosetta and
-            preferred_series is not None)
+        enabled = (service_uses_launchpad(self.context.translations_usage)
+            and preferred_series is not None)
         link = ''
         if enabled:
             link = canonical_url(
-                preferred_series, rootsite='translations', view_name='+export')
+                preferred_series,
+                rootsite='translations',
+                view_name='+export')
             text = 'Download "%s"' % preferred_series.name
 
         return Link(link, text, icon='download', enabled=enabled)
@@ -90,19 +93,19 @@
     @cachedproperty
     def uses_translations(self):
         """Whether this product has translatable templates."""
-        return (self.context.official_rosetta and
-                self.primary_translatable is not None)
+        return (service_uses_launchpad(self.context.translations_usage)
+                and self.primary_translatable is not None)
 
     @cachedproperty
     def no_translations_available(self):
         """Has no translation templates but does support translations."""
-        return (self.context.official_rosetta and
-                self.primary_translatable is None)
+        return (service_uses_launchpad(self.context.translations_usage)
+                and self.primary_translatable is None)
 
     @cachedproperty
     def show_page_content(self):
         """Whether the main content of the page should be shown."""
-        return (self.context.official_rosetta or
+        return (service_uses_launchpad(self.context.translations_usage) or
                 check_permission("launchpad.TranslationsAdmin", self.context))
 
     @cachedproperty

=== modified file 'lib/lp/translations/browser/productseries.py'
--- lib/lp/translations/browser/productseries.py	2010-08-24 10:45:57 +0000
+++ lib/lp/translations/browser/productseries.py	2010-09-17 01:08:50 +0000
@@ -250,7 +250,8 @@
                     'administrator in the coming few days.  You can track '
                     'your upload\'s status in the '
                     '<a href="%s/+imports">Translation Import Queue</a>' %(
-                        canonical_url(self.context, rootsite='translations'))))
+                        canonical_url(self.context,
+                        rootsite='translations'))))
 
         elif is_tar_filename(filename):
             # Add the whole tarball to the import queue.
@@ -277,7 +278,8 @@
                     'your upload\'s status in the '
                     '<a href="%s/+imports">Translation Import Queue</a>' %(
                         num, plural_s, plural_s, itthey,
-                        canonical_url(self.context, rootsite='translations'))))
+                        canonical_url(self.context,
+                        rootsite='translations'))))
                 if len(conflicts) > 0:
                     if len(conflicts) == 1:
                         warning = (
@@ -492,6 +494,10 @@
         super(ProductSeriesTemplatesView, self).initialize(
             series=self.context, is_distroseries=False)
 
+    def constructTemplateURL(self, template):
+        """See `BaseSeriesTemplatesView`."""
+        return '+pots/%s' % template.name
+
 
 class LinkTranslationsBranchView(LaunchpadEditFormView):
     """View to set the series' translations export branch."""

=== modified file 'lib/lp/translations/browser/tests/language-views.txt'
--- lib/lp/translations/browser/tests/language-views.txt	2010-07-16 16:51:52 +0000
+++ lib/lp/translations/browser/tests/language-views.txt	2010-09-17 01:08:50 +0000
@@ -143,7 +143,10 @@
 
 Create a product, a template with one msgset and a pofile
 
-    >>> product = factory.makeProduct(official_rosetta=True)
+    >>> from lp.app.enums import ServiceUsage
+
+    >>> product = factory.makeProduct(
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> template = factory.makePOTemplate(
     ...     productseries=product.getSeries('trunk'))
     >>> potmsgset = factory.makePOTMsgSet(template)

=== modified file 'lib/lp/translations/browser/tests/test_breadcrumbs.py'
--- lib/lp/translations/browser/tests/test_breadcrumbs.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/browser/tests/test_breadcrumbs.py	2010-09-17 01:08:50 +0000
@@ -6,6 +6,7 @@
 from zope.component import getUtility
 
 from canonical.lazr.utils import smartquote
+from lp.app.enums import ServiceUsage
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.testing.breadcrumbs import BaseBreadcrumbTestCase
 from lp.testing.factory import remove_security_proxy_and_shout_at_engineer
@@ -152,7 +153,7 @@
     def test_potemplate(self):
         product = self.factory.makeProduct(
             name='crumb-tester', displayname="Crumb Tester",
-            official_rosetta=True)
+            translations_usage=ServiceUsage.LAUNCHPAD)
         series = self.factory.makeProductSeries(
             name="test", product=product)
         potemplate = self.factory.makePOTemplate(
@@ -176,7 +177,7 @@
     def test_pofiletranslate(self):
         product = self.factory.makeProduct(
             name='crumb-tester', displayname="Crumb Tester",
-            official_rosetta=True)
+            translations_usage=ServiceUsage.LAUNCHPAD)
         series = self.factory.makeProductSeries(name="test", product=product)
         potemplate = self.factory.makePOTemplate(series, name="test-template")
         pofile = self.factory.makePOFile('eo', potemplate)

=== added file 'lib/lp/translations/browser/tests/test_seriestemplatesview.py'
--- lib/lp/translations/browser/tests/test_seriestemplatesview.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/browser/tests/test_seriestemplatesview.py	2010-09-17 01:08:50 +0000
@@ -0,0 +1,332 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `BaseSeriesTemplatesView` and descendants."""
+
+__metaclass__ = type
+
+import re
+
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.launchpad.ftests import login
+from canonical.launchpad.webapp.publisher import canonical_url
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
+from canonical.testing import DatabaseFunctionalLayer
+from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.productseries import ProductSeries
+from lp.translations.browser.distroseries import DistroSeriesTemplatesView
+from lp.translations.browser.productseries import ProductSeriesTemplatesView
+from lp.testing import (
+    login_person,
+    TestCaseWithFactory,
+    )
+from lp.testing.sampledata import ADMIN_EMAIL
+
+
+class SeriesTemplatesViewScenario:
+
+    # The view class to test.
+    view_class = None
+
+    # The expected list of columns for the view_class, each shown as a
+    # list holding the column's CSS class.
+    columns = None
+
+    def makeTemplateContext(self):
+        """Create makePOTemplate arguments to create a series."""
+        raise NotImplementedError()
+
+    def _makeTemplate(self, **kwargs):
+        """Create a distro- or productseries for the view."""
+        args_dict = self.makeTemplateContext()
+        args_dict.update(kwargs)
+        return self.factory.makePOTemplate(**args_dict)
+
+    def _getSeries(self, template):
+        """Get `template`'s distro- or productseries."""
+        return template.distroseries or template.productseries
+
+    def _makeView(self, template=None):
+        """Create a `BaseTemplatesView` containing `template`."""
+        if template is None:
+            template = self._makeTemplate()
+        request = LaunchpadTestRequest()
+        view = self.view_class(self._getSeries(template), request)
+        view.initialize()
+        return view
+
+    def _findTagClasses(self, html, tag):
+        """Find the CSS classes for all instances of `tag`s in `html`.
+
+        Returns a list of lists.  The outer list represents instances of
+        `tag`, in the order in which they are found.  The inner lists
+        hold the respective sets of HTML classes for these tags, sorted
+        alphabetically.
+        """
+        regex = '<%s [^>]*class="([^"]*)"' % tag
+        return [
+            sorted(css_class.split())
+            for css_class in re.findall(regex, html)]
+
+    def _findActions(self, html):
+        """Find the available actions in an HTML actions column."""
+        return re.findall('<[^>]*>([^<]*)</[^>]*', html)
+
+    def test_has_the_right_columns(self):
+        # Test the column headers against the expected list.
+        view = self._makeView()
+        header = view.renderTemplatesHeader()
+        self.assertEqual(self.columns, self._findTagClasses(header, 'th'))
+
+    def test_logging_in_adds_actions_column(self):
+        # A logged-in user gets to see an extra "actions" column.
+        template = self._makeTemplate()
+        login_person(self.factory.makePerson())
+        view = self._makeView(template)
+        columns = self.columns + [['actions_column']]
+        header = view.renderTemplatesHeader()
+        self.assertEqual(columns, self._findTagClasses(header, 'th'))
+        row = view.renderTemplateRow(template)
+        self.assertEqual(columns, self._findTagClasses(row, 'td'))
+
+    def test_user_actions(self):
+        # The only action offered to regular users is Download.
+        template = self._makeTemplate()
+        url = canonical_url(template)
+        login_person(self.factory.makePerson())
+        view = self._makeView(template)
+
+        self.assertEqual(
+            ['Download'],
+            self._findActions(view._renderActionsColumn(template, url)))
+
+    def test_admin_actions(self):
+        # An administrator gets to see all actions on a template.
+        template = self._makeTemplate()
+        url = canonical_url(template)
+        login(ADMIN_EMAIL)
+        view = self._makeView(template)
+
+        self.assertEqual(
+            ['Edit', 'Upload', 'Download', 'Administer'],
+            self._findActions(view._renderActionsColumn(template, url)))
+
+    def test_edit_actions(self):
+        # A non-admin user with edit rights gets the Edit, Upload, and
+        # Download actions.
+        template = self._makeTemplate()
+        url = canonical_url(template)
+        login_person(self.factory.makePerson())
+        view = self._makeView(template)
+        view.can_edit = True
+
+        self.assertEqual(
+            ['Edit', 'Upload', 'Download'],
+            self._findActions(view._renderActionsColumn(template, url)))
+
+    def test_constructs_correct_urls(self):
+        # The view classes can override constructTemplateURL with
+        # optimized versions.  These can produce either an absolute URL
+        # that exactly matches the template's canonical_url, or a
+        # relative one starting from the series' canonical_url.
+        template = self._makeTemplate()
+        view = self._makeView(template)
+
+        series_url = canonical_url(
+            self._getSeries(template), rootsite='translations')
+        constructed_url = view.constructTemplateURL(template)
+
+        self.assertIn(
+            canonical_url(template),
+            (constructed_url, '/'.join([series_url, constructed_url])))
+
+    def test_renderTemplateLink(self):
+        # _renderTemplateLink renders a link to the template.
+        template = self._makeTemplate()
+        view = self._makeView(template)
+
+        url = view.constructTemplateURL(template)
+        link = view._renderTemplateLink(template, url)
+
+        self.assertIn('<a ', link)
+        self.assertIn('href="%s"' % url, link)
+        self.assertIn('>%s<' % template.name, link)
+
+    def test_renderTemplateLink_marks_disabled(self):
+        # _renderTemplateLinks marks disabled templates as "(inactive)."
+        template = self._makeTemplate()
+        view = self._makeView(template)
+        url = canonical_url(template)
+
+        removeSecurityProxy(template).iscurrent = True
+        self.assertNotIn(
+            '(inactive)', view._renderTemplateLink(template, url))
+        removeSecurityProxy(template).iscurrent = False
+        self.assertIn('(inactive)', view._renderTemplateLink(template, url))
+
+    def test_renderLastUpdateDate_sets_sortkey(self):
+        # _renderLastUpdateDate sets the full date as the column's sort
+        # key, so that clicking on the column header sorts by date (even
+        # if sorting alphabetically by the visible date might produce a
+        # different ordering).
+        template = self._makeTemplate()
+        view = self._makeView(template)
+
+        date_field = view._renderLastUpdateDate(template)
+
+        # The sort key is set in a span of class "sortkey."
+        sortkey_match = re.findall(
+            '<span class="sortkey">([^<]*)</span>', date_field)
+        self.assertIsNot(None, sortkey_match)
+        self.assertEqual(1, len(sortkey_match))
+
+        # The column also has the same full date as a tooltip.
+        full_date = sortkey_match[0].strip()
+        self.assertIn('title="%s"' % full_date, date_field)
+
+    def test_renderAction_returns_empty_string_if_not_enabled(self):
+        view = self._makeView()
+        self.assertEqual(
+            '',
+            view._renderAction('url', 'name', 'path', 'sprite', False))
+
+    def test_renderAction(self):
+        # If enabled, _renderAction produces a link to an action form
+        # for a given template.
+        view = self._makeView()
+
+        url = self.factory.getUniqueString()
+        name = self.factory.getUniqueString()
+        path = self.factory.getUniqueString()
+        sprite = self.factory.getUniqueString()
+
+        action = view._renderAction(url, name, path, sprite, True)
+
+        self.assertIn('<a ', action)
+        self.assertIn('href="%s/%s"' % (url, path), action)
+        self.assertIn(name, action)
+        self.assertIn('class="sprite %s"' % sprite, action)
+
+    def test_renderField_returns_empty_string_if_no_content(self):
+        view = self._makeView()
+        self.assertEqual('', view._renderField('x', None, tag='y'))
+
+    def test_renderField_returns_empty_field_for_empty_content(self):
+        field = self._makeView()._renderField('class', '', tag='tag')
+        self.assertIn('<tag class="class">', field)
+        self.assertIn('</tag>', field)
+
+    def test_renderField(self):
+        column_class = self.factory.getUniqueString()
+        content = self.factory.getUniqueString()
+        tag = self.factory.getUniqueString()
+
+        field = self._makeView()._renderField(column_class, content, tag=tag)
+
+        self.assertIn('<%s class="%s">' % (tag, column_class), field)
+        self.assertIn(content, field)
+        self.assertIn('</%s>' % tag, field)
+
+    def test_renderTemplateRow(self):
+        template = self._makeTemplate()
+        view = self._makeView(template)
+
+        row = view.renderTemplateRow(template)
+
+        self.assertEqual(
+            [sorted(['template_row', view.rowCSSClass(template)])],
+            self._findTagClasses(row, 'tr'))
+
+        self.assertEqual(self.columns, self._findTagClasses(row, 'td'))
+
+
+class TestDistroSeriesTemplatesView(SeriesTemplatesViewScenario,
+                                    TestCaseWithFactory):
+    """Run the test scenario against `DistroSeriesTemplatesView`."""
+
+    layer = DatabaseFunctionalLayer
+
+    view_class = DistroSeriesTemplatesView
+
+    columns = [
+        ['priority_column'],
+        ['sourcepackage_column'],
+        ['template_column'],
+        ['length_column'],
+        ['lastupdate_column'],
+    ]
+
+    def makeTemplateContext(self):
+        """See `SeriesTemplatesViewScenario`."""
+        return dict(
+            sourcepackagename=self.factory.makeSourcePackageName(),
+            distroseries=self.factory.makeDistroSeries())
+
+    def test_makeTemplate(self):
+        # In this test case, _makeTemplate produces a distroseries
+        # template.
+        template = self._makeTemplate()
+        self.assertIsInstance(template.distroseries, DistroSeries)
+        self.assertIs(None, template.productseries)
+
+    def test_findTagClasses(self):
+        # Tested here arbitrarily (no need to repeat it): the
+        # _findTagClasses helper.
+        self.assertEqual(
+            [['b', 'c'], ['a']],
+            self._findTagClasses('<x class="c b" /><x class="a">', 'x'))
+
+    def test_findActions(self):
+        # Tested here arbitrarily (no need to repeat it): the
+        # _findActions helper.
+        self.assertEqual(['Foo'], self._findActions('<a class="bar">Foo</a>'))
+
+    def test_is_distroseries(self):
+        self.assertTrue(self._makeView().is_distroseries)
+
+    def test_renderSourcePackage(self):
+        # _renderSourcePackage returns the template's source-package
+        # name for a productseries view.
+        template = self._makeTemplate()
+        view = self._makeView(template)
+
+        return self.assertEqual(
+            template.sourcepackagename.name,
+            view._renderSourcePackage(template))
+
+
+class TestProductSeriesTemplatesView(SeriesTemplatesViewScenario,
+                                     TestCaseWithFactory):
+    """Run the test scenario against `ProductSeriesTemplatesView`."""
+
+    layer = DatabaseFunctionalLayer
+
+    view_class = ProductSeriesTemplatesView
+
+    columns = [
+        ['priority_column'],
+        ['template_column'],
+        ['length_column'],
+        ['lastupdate_column'],
+    ]
+
+    def makeTemplateContext(self):
+        """See `SeriesTemplatesViewScenario`."""
+        return dict(productseries=self.factory.makeProductSeries())
+
+    def test_makeTemplate(self):
+        # In this test case, _makeTemplate produces a productseries
+        # template.
+        template = self._makeTemplate()
+        self.assertIs(None, template.distroseries)
+        self.assertIsInstance(template.productseries, ProductSeries)
+
+    def test_is_distroseries(self):
+        self.assertFalse(self._makeView().is_distroseries)
+
+    def test_renderSourcePackage(self):
+        # _renderSourcePackage returns None for a productseries view.
+        template = self._makeTemplate()
+        view = self._makeView(template)
+        self.assertIs(None, view._renderSourcePackage(template))

=== modified file 'lib/lp/translations/doc/potmsgset.txt'
--- lib/lp/translations/doc/potmsgset.txt	2010-09-01 10:03:11 +0000
+++ lib/lp/translations/doc/potmsgset.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= POTMsgSet tests =
+POTMsgSet tests
+===============
 
 POTMsgSet represents messages to translate that a POTemplate file has.
 
@@ -17,7 +18,9 @@
     ...     ILanguageSet, IPersonSet, IProductSet)
 
     >>> potemplate = factory.makePOTemplate()
-    >>> potmsgset = factory.makePOTMsgSet(potemplate=potemplate, singular="bla")
+    >>> potmsgset = factory.makePOTMsgSet(
+    ...     potemplate=potemplate,
+    ...     singular="bla")
 
 Verify interface.
 
@@ -31,7 +34,8 @@
     >>> import pytz
 
 
-== POTMsgSet.is_translation_credit and POTMsgSet.translation_credit_type ==
+POTMsgSet.is_translation_credit and POTMsgSet.translation_credit_type
+---------------------------------------------------------------------
 
 A POTMsgSet can be translation credits. These have special msgids that may
 differ for historical reason for the same type of credit. The property
@@ -51,7 +55,8 @@
     Gnome credits message
 
 
-== POTMsgSet.normalizeWhitespaces ==
+POTMsgSet.normalizeWhitespaces
+------------------------------
 
 This function copies the leading and trailing whitespaces from
 POTMsgSet's msgid into the 'text' argument.
@@ -96,7 +101,8 @@
     True
 
 
-== POTMsgSet.convertDotToSpace ==
+POTMsgSet.convertDotToSpace
+---------------------------
 
 This method changes the u'\u2022' char by the normal white space.
 
@@ -126,14 +132,17 @@
     u'a string with the char \u2022'
 
 
-== POTMsgSet.normalizeNewLines ==
+POTMsgSet.normalizeNewLines
+---------------------------
 
 This method syncs the new line chars to use the same as the associated msgid.
 
 As this test is too specific, we are going to change a msgid as we need for
 every test.
 
-    >>> potmsgset_windows = factory.makePOTMsgSet(potemplate, singular=u'\r\n')
+    >>> potmsgset_windows = factory.makePOTMsgSet(
+    ...                             potemplate,
+    ...                             singular=u'\r\n')
     >>> potmsgset_unix = factory.makePOTMsgSet(potemplate, singular=u'\n')
     >>> potmsgset_mac = factory.makePOTMsgSet(potemplate, singular=u'\r')
     >>> text_windows = u'\r\n'
@@ -180,7 +189,8 @@
     BrokenTextError: ...
 
 
-== POTMsgSet.applySanityFixes ==
+POTMsgSet.applySanityFixes
+--------------------------
 
 This function applies all checks we know to fix broken input
 
@@ -199,7 +209,8 @@
     u'\nTranslation\nto test\n'
 
 
-== POTMsgSet.updateTranslation ==
+POTMsgSet.updateTranslation
+---------------------------
 
 This method is the core piece of Translations infrastructure: it
 updates a single language translation for a POTMsgSet, and modifies a
@@ -309,7 +320,8 @@
     >>> current.date_created is None
     False
 
-=== Unsetting a translation from the import ===
+Unsetting a translation from the import
+.......................................
 
 A translation can be removed from the import by being set to an empty string.
 
@@ -349,7 +361,8 @@
     >>> upstream_pofile.updateStatistics()
     (0, 0, 0, 2)
 
-=== Activating an existing suggestion ===
+Activating an existing suggestion
+.................................
 
 Foo Bar is a privileged translator who can do reviews and submit translations
 directly, while No Privileges user can only submit suggestions.
@@ -438,7 +451,8 @@
     u'Foo Bar'
 
 
-== Plural forms ==
+Plural forms
+------------
 
 Let's focus on handling of messages with plural forms.
 
@@ -513,7 +527,8 @@
     [None, None, None]
 
 
-=== Missing forms ===
+Missing forms
+.............
 
 Even when a message has a singular and a plural in English, a
 translation does not have to cover all plural forms available in the
@@ -538,7 +553,8 @@
     False
 
 
-=== Extraneous forms ===
+Extraneous forms
+................
 
 It's not normally possible to input more plural forms for a translated
 message than the language has.  But that number is configurable, and can
@@ -614,7 +630,8 @@
 possible.
 
 
-== isTranslationNewerThan ==
+isTranslationNewerThan
+----------------------
 
 This method tells us whether the active translation was reviewed after
 the given timestamp.
@@ -633,7 +650,8 @@
     False
 
 
-== External translation suggestions ==
+External translation suggestions
+--------------------------------
 
 External translation suggestions are current, imported or suggested
 translation for exactly the same English string, but in a different
@@ -670,7 +688,8 @@
     ...         print line
 
 
-== POTMsgSet.getExternallyUsedTranslationMessages ==
+POTMsgSet.getExternallyUsedTranslationMessages
+----------------------------------------------
 
  On one side, we have a translation template for the evolution product.
 
@@ -692,10 +711,10 @@
 
 Both, product and distribution use Launchpad Translations.
 
-    >>> evolution.official_rosetta
-    True
-    >>> ubuntu.official_rosetta
-    True
+    >>> evolution.translations_usage.name
+    'LAUNCHPAD'
+    >>> ubuntu.translations_usage.name
+    'LAUNCHPAD'
 
 And both translation templates are current
 
@@ -754,7 +773,9 @@
 The same happens if the distribution is not officially using
 translations.
 
-    >>> ubuntu.official_rosetta = False
+    >>> from lp.app.enums import ServiceUsage
+
+    >>> ubuntu.translations_usage = ServiceUsage.NOT_APPLICABLE
 
     # We set the template as current again so we are sure that we don't show
     # suggestions just due to the change to the official_rosetta flag.
@@ -767,7 +788,7 @@
 
 And products not using translations officially have the same behaviour.
 
-    >>> evolution.official_rosetta = False
+    >>> evolution.translations_usage = ServiceUsage.NOT_APPLICABLE
     >>> transaction.commit()
     >>> suggestions = evo_distro_message.getExternallyUsedTranslationMessages(
     ...    spanish)
@@ -776,12 +797,13 @@
 
 Let's restore the flags for next section.
 
-    >>> ubuntu.official_rosetta = True
-    >>> evolution.official_rosetta = True
+    >>> ubuntu.translations_usage = ServiceUsage.LAUNCHPAD
+    >>> evolution.translations_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
 
 
-== POTMsgSet.getExternallySuggestedTranslationMessages ==
+POTMsgSet.getExternallySuggestedTranslationMessages
+---------------------------------------------------
 
 This method returns a set of submissions that have suggested translations
 for the same msgid as the given POTMsgSet across the whole system.
@@ -876,7 +898,7 @@
     # We set the template as current again so we are sure that we don't show
     # suggestions just due to the change to the official_rosetta flag.
     >>> potmsgset_translated.potemplate.iscurrent = True
-    >>> ubuntu.official_rosetta = False
+    >>> ubuntu.translations_usage = ServiceUsage.NOT_APPLICABLE
     >>> transaction.commit()
 
     >>> wiki_submissions = (
@@ -886,7 +908,8 @@
     0
 
 
-== POTMsgSet.getCurrentDummyTranslationMessage ==
+POTMsgSet.getCurrentDummyTranslationMessage
+-------------------------------------------
 
 Sometimes, there are POTMsgSet objects with no translations to a language,
 and we need to get dummy objects which emulate them to do read operations.
@@ -908,7 +931,8 @@
     AssertionError: There is already a translation message ...
 
 
-== Suggestions for translator credits ==
+Suggestions for translator credits
+----------------------------------
 
 Messages with translator credits are translated automatically by
 Launchpad, so we should not get any suggestions for them.
@@ -936,8 +960,8 @@
 
 Now, let's add 'translation-credits' message to a different POTemplate:
 
-     >>> new_credits = evolution_potemplate.createMessageSetFromText(
-     ...     singular_text=u'translation-credits', plural_text=None)
+    >>> new_credits = evolution_potemplate.createMessageSetFromText(
+    ...     singular_text=u'translation-credits', plural_text=None)
 
 However, this one doesn't show up as external suggestion for Spanish.
 
@@ -946,7 +970,8 @@
     >>> new_credits.getExternallySuggestedTranslationMessages(spanish)
     []
 
-== POTMsgSet.setSequence ==
+POTMsgSet.setSequence
+---------------------
 
 Finally, the new `IPOTMsgSet` should have an entry in the
 `TranslationTemplateItem` table once we assign a sequence number.
@@ -998,7 +1023,8 @@
     0
 
 
-== POTMsgSet.flags ==
+POTMsgSet.flags
+---------------
 
 The gettext format can associate flags with a POTMsgSet, such as "this
 is a fuzzily matched message" or "this message follows C format-string

=== modified file 'lib/lp/translations/doc/translationimportqueue.txt'
--- lib/lp/translations/doc/translationimportqueue.txt	2010-07-13 21:49:34 +0000
+++ lib/lp/translations/doc/translationimportqueue.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= TranslationImportQueueEntry =
+TranslationImportQueueEntry
+===========================
 
 The TranslationImportQueueEntry is an entry of the queue that will be imported
 into Rosetta.
@@ -14,7 +15,8 @@
     ...     store.find(TranslationImportQueueEntry).remove()
 
 
-== getGuessedPOFile ==
+getGuessedPOFile
+----------------
 
 This property gives us the IPOFile where we think we should import this entry.
 
@@ -59,7 +61,8 @@
     ...     u'po/sr.po', 'foo', True, rosetta_experts,
     ...      productseries=evolution_productseries)
 
-This entry has no information about the IPOFile where it should be attached to:
+This entry has no information about the IPOFile where it should be attached
+to:
 
     >>> entry.import_into is None
     True
@@ -82,7 +85,8 @@
     ...      sourcepackagename=evolution_sourcepackagename)
     >>> transaction.commit()
 
-This entry has no information about the IPOFile where it should be attached to:
+This entry has no information about the IPOFile where it should be attached
+to:
 
     >>> entry.import_into is None
     True
@@ -225,7 +229,8 @@
     >>> pofile.path
     u'po/sr.po'
 
-=== getGuessedPOFile with KDE ===
+getGuessedPOFile with KDE
+.........................
 
 Official KDE packages have a non standard layout where the .pot files are
 stored inside the sourcepackage with the binaries that will use it and the
@@ -363,7 +368,8 @@
     >>> transaction.abort()
 
 
-=== getGuessedPOFile with KOffice ===
+getGuessedPOFile with KOffice
+.............................
 
 Like official KDE packages, KOffice stores the .pot and .po files in different
 packages, the only difference it has is that there is just one source package
@@ -403,7 +409,9 @@
 
 And set this entry as already imported.
 
-    >>> koffice_pot_entry.setStatus(RosettaImportStatus.IMPORTED, rosetta_experts)
+    >>> koffice_pot_entry.setStatus(
+    ...     RosettaImportStatus.IMPORTED,
+    ...     rosetta_experts)
     >>> flush_database_updates()
 
 Let's attach a .po file from koffice-l10n
@@ -449,7 +457,8 @@
     >>> transaction.abort()
 
 
-=== getGuessedPOFile with .po files in different directories ===
+getGuessedPOFile with .po files in different directories
+........................................................
 
 Some packages have translations and templates inside the same package, but
 they don't have them inside the same directory. The layout is:
@@ -500,7 +509,9 @@
 
 And set this entry as already imported.
 
-    >>> adept_pot_entry.setStatus(RosettaImportStatus.IMPORTED, rosetta_experts)
+    >>> adept_pot_entry.setStatus(
+    ...     RosettaImportStatus.IMPORTED,
+    ...     rosetta_experts)
     >>> flush_database_updates()
 
 Let's attach a .po file now.
@@ -551,7 +562,9 @@
 
 And set this entry as already imported.
 
-    >>> ktorrent_pot_entry.setStatus(RosettaImportStatus.IMPORTED, rosetta_experts)
+    >>> ktorrent_pot_entry.setStatus(
+    ...     RosettaImportStatus.IMPORTED,
+    ...     rosetta_experts)
     >>> flush_database_updates()
 
 Let's attach a .po file now.
@@ -739,7 +752,8 @@
     >>> transaction.abort()
 
 
-== executeOptimisticBlock ==
+executeOptimisticBlock
+----------------------
 
 This method looks on the queue to find entries to block based on other .pot
 entries that are stored on the same directory and are already blocked.
@@ -803,7 +817,8 @@
     True
 
 On the other hand, this other one is for the same
-distroseries/sourcepackagename than the .pot file we have so it's also blocked.
+distroseries/sourcepackagename than the .pot file we have so it's also
+blocked.
 
     >>> entry4 = translationimportqueue.get(4)
     >>> entry4.path
@@ -820,7 +835,8 @@
     True
 
 
-== getElapsedTimeText ==
+getElapsedTimeText
+-----------------
 
 This method returns a string representing the elapsed time since the entry
 was added to the queue.
@@ -860,13 +876,15 @@
     '2 days 13 hours 5 minutes ago'
 
 
-= TranslationImportQueue =
+TranslationImportQueue
+======================
 
 The translation import queue is the place where the new translation imports
 end before being imported into Rosetta.
 
 
-== getTemplatesOnSameDirectory ==
+getTemplatesOnSameDirectory
+---------------------------
 
 This method allows us to get the set of .pot files we have on the same
 directory that a given entry.
@@ -905,7 +923,8 @@
     0
 
 
-== getRequestTargets ==
+getRequestTargets
+-----------------
 
     >>> # Helper functions
     >>> def get_target_names(status=None):
@@ -1069,7 +1088,8 @@
     firefox         Mozilla Firefox
     ubuntu/hoary    Hoary
 
-== addOrUpdateEntry() ==
+addOrUpdateEntry()
+------------------
 
 addOrUpdateEntry adds a new entry to the import queue so we can handle it
 later with poimport script.
@@ -1264,7 +1284,8 @@
     >>> print entry.content.read()
     New bar content
 
-= Filename filters =
+Filename filters
+================
 
 A tarball doesn't always have everything in quite the right place.  If
 you need to manipulate the file paths within a tarball before the files
@@ -1307,7 +1328,8 @@
     netapplet       | None               | new-directory/fr.po
 
 
-= Invalid data =
+Invalid data
+============
 
 If administrators fail to correct certain errors in requests while approving
 them, and the admin user interface mistakenly accepts the approval, we may
@@ -1385,7 +1407,8 @@
     >>> clear_queue(translationimportqueue)
 
 
-== getRequestTargets output ordering ==
+getRequestTargets output ordering
+---------------------------------
 
 The queue is populated with a wild mix of requests: for packages in
 different release series of Ubuntu, for packages in different distros,
@@ -1472,7 +1495,8 @@
     evolution           Evolution
 
 
-== cleanUpQueue ==
+cleanUpQueue
+------------
 
 The queue is cleaned up regularly.
 
@@ -1489,7 +1513,8 @@
     0
 
 
-=== State and Age ===
+State and Age
+.............
 
 Entries can be cleaned up because they have been in a specific state for
 at least a specified period of time.
@@ -1516,18 +1541,22 @@
     >>> print_queue_entries(translationimportqueue)
 
 
-=== Deactivated Products ===
+Deactivated Products
+....................
 
 Another reason for deleting entries is that they belong to products that
 have been deactivated.
 
 A user sets up Jokosher for translation, and uploads a template.
 
+    >>> from lp.app.enums import ServiceUsage
+
     >>> jokosher = productset['jokosher']
     >>> jokosher_trunk = jokosher.getSeries('trunk')
-    >>> jokosher.official_rosetta = True
+    >>> jokosher.translations_usage = ServiceUsage.LAUNCHPAD
     >>> syncUpdate(jokosher)
-    >>> jokosher_subset = potemplateset.getSubset(productseries=jokosher_trunk)
+    >>> jokosher_subset = potemplateset.getSubset(
+    ...     productseries=jokosher_trunk)
     >>> template = jokosher_subset.new(
     ...     'jokosher', 'jokosher', 'jokosher.pot', rosetta_experts)
     >>> syncUpdate(template)

=== modified file 'lib/lp/translations/doc/translationmessage-destroy.txt'
--- lib/lp/translations/doc/translationmessage-destroy.txt	2009-07-02 17:16:50 +0000
+++ lib/lp/translations/doc/translationmessage-destroy.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-== destroySelf ==
+destroySelf
+===========
 
 (Note: this test runs as rosettaadmin to obtain the necessary
 privileges)
@@ -26,7 +27,8 @@
     SQLObjectNotFound:...
 
 
-== POFileTranslator update on remove ==
+POFileTranslator update on remove
+=================================
 
 In two sharing POTemplates with one shared POTMsgSet with one shared
 translation, we get two POFileTranslator records for each of the POFiles.
@@ -35,16 +37,17 @@
     >>> # a global 'postgres' permission which allows everything.
     >>> LaunchpadZopelessLayer.switchDbUser('postgres')
     >>> from canonical.database.sqlbase import sqlvalues
+    >>> from lp.app.enums import ServiceUsage
     >>> from lp.translations.model.pofiletranslator import POFileTranslator
     >>> from lp.testing.factory import LaunchpadObjectFactory
     >>> factory = LaunchpadObjectFactory()
 
-    >>> foo = factory.makeProduct()
+    >>> foo = factory.makeProduct(
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> foo_devel = factory.makeProductSeries(
     ...     name='devel', product=foo)
     >>> foo_stable = factory.makeProductSeries(
     ...     name='stable', product=foo)
-    >>> foo.official_rosetta = True
     >>> devel_potemplate = factory.makePOTemplate(
     ...     productseries=foo_devel, name="messages")
     >>> stable_potemplate = factory.makePOTemplate(foo_stable,
@@ -56,7 +59,9 @@
     >>> potmsgset = factory.makePOTMsgSet(devel_potemplate, sequence=1)
     >>> potmsgset.setSequence(stable_potemplate, 1)
     >>> tm = factory.makeTranslationMessage(
-    ...     pofile=devel_sr_pofile, potmsgset=potmsgset, translations=[u"blah"])
+    ...     pofile=devel_sr_pofile,
+    ...     potmsgset=potmsgset,
+    ...     translations=[u"blah"])
     >>> print POFileTranslator.select(
     ...     "latest_message=%s" % sqlvalues(tm)).count()
     2

=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
--- lib/lp/translations/doc/translations-export-to-branch.txt	2010-07-20 17:50:45 +0000
+++ lib/lp/translations/doc/translations-export-to-branch.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= Exporting translations to a bzr branch =
+Exporting translations to a bzr branch
+======================================
 
 The translations-export-to-branch script visits all ProductSeries with a
 translations_branch set, and for each, exports the series' translations
@@ -76,10 +77,11 @@
 branch.
 
     >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.app.enums import ServiceUsage
 
     >>> gazblachko = removeSecurityProxy(factory.makeProduct(
     ...     name='gazblachko', displayname='Gazblachko'))
-    >>> gazblachko.official_rosetta = True
+    >>> gazblachko.translations_usage = ServiceUsage.LAUNCHPAD
 
     >>> branch = removeSecurityProxy(factory.makeBranch(
     ...     name='gazpo', owner=gazblachko.owner, product=gazblachko))
@@ -162,13 +164,13 @@
 When Gazblachko stops using Launchpad for Translations, the exports stop
 also.
 
-    >>> gazblachko.official_rosetta = False
+    >>> gazblachko.translations_usage = ServiceUsage.NOT_APPLICABLE
     >>> transaction.commit()
     >>> script.main()
     INFO Exporting to translations branches.
     INFO Processed 0 item(s); 0 failure(s), 0 unpushed branch(es).
 
-    >>> gazblachko.official_rosetta = True
+    >>> gazblachko.translations_usage = ServiceUsage.LAUNCHPAD
     >>> transaction.commit()
 
 
@@ -271,7 +273,8 @@
 collected for import yet.
 
 
-=== Branch races ===
+Branch races
+............
 
 Any translations coming in through a branch push are safe once they're
 in the translations import queue.  So the race window spans from the
@@ -299,7 +302,8 @@
     INFO Processed 1 item(s); 1 failure(s), 0 unpushed branch(es).
 
 
-=== Pending imports from same branch ===
+Pending imports from same branch
+................................
 
 Another race condition is detected by the script itself: there may be
 pending translations BranchJobs on the branch.

=== modified file 'lib/lp/translations/doc/translationsoverview.txt'
--- lib/lp/translations/doc/translationsoverview.txt	2009-07-24 12:55:03 +0000
+++ lib/lp/translations/doc/translationsoverview.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= TranslationsOverview =
+TranslationsOverview
+====================
 
 This class provides a basic overview of the Launchpad Translations component.
 It includes data such as projects which have so far received the most
@@ -45,14 +46,16 @@
     18
 
 
-== _normalizeSizes ==
+_normalizeSizes
+---------------
 
 This private method accepts a list of tuples (object, size) and
 normalizes `size` values into the range [MINIMUM_SIZE, MAXIMUM_SIZE].
 
     >>> test_list = [('one', 3), ('two', 0), ('three', 1)]
     >>> from zope.security.proxy import removeSecurityProxy
-    >>> result = removeSecurityProxy(overview)._normalizeSizes(test_list, 0, 3)
+    >>> naked_overview = removeSecurityProxy(overview)
+    >>> result = naked_overview._normalizeSizes(test_list, 0, 3)
     >>> for pillar in result:
     ...     print "%s: %d" % (pillar['pillar'], pillar['weight'])
     one: 18
@@ -60,7 +63,8 @@
     three: 13
 
 
-== Getting the most translated pillars ==
+Getting the most translated pillars
+-----------------------------------
 
 Method getMostTranslatedPillars() returns a list of dicts listing
 pillars with most translations karma so far, along with a relative
@@ -104,10 +108,13 @@
 
 Adding a little bit of karma to upstart will put it in the list as well.
 
+    >>> from lp.app.enums import ServiceUsage
+
     >>> start_karma_update()
     >>> upstart = product_set.getByName('upstart')
     >>> upstart_id = upstart.id
-    >>> removeSecurityProxy(upstart).official_rosetta = True
+    >>> naked_upstart = removeSecurityProxy(upstart)
+    >>> naked_upstart.translations_usage = ServiceUsage.LAUNCHPAD
     >>> cache_entry = karmacachemanager.new(
     ...     50, carlos.id, translations.id, product_id=upstart_id)
     >>> finish_karma_update()
@@ -153,7 +160,8 @@
     Ubuntu: 24
 
 
-== Zero karma ==
+Zero karma
+----------
 
 Sometimes a pillar appears to be listed in the karma cache with zero
 karma.  Our algorithm takes the logarithm of its karma, but it's

=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py	2010-09-03 03:12:39 +0000
+++ lib/lp/translations/model/potemplate.py	2010-09-17 01:08:50 +0000
@@ -64,6 +64,7 @@
     IMasterStore,
     IStore,
     )
+from lp.app.enums import service_uses_launchpad
 from lp.app.errors import NotFoundError
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.model.sourcepackagename import SourcePackageName
@@ -1322,8 +1323,7 @@
             preferred_matches = [
                 match
                 for match in matches
-                if match.from_sourcepackagename == sourcepackagename
-            ]
+                if match.from_sourcepackagename == sourcepackagename]
 
             if len(preferred_matches) == 1:
                 return preferred_matches[0]
@@ -1378,6 +1378,9 @@
 
     def populateSuggestivePOTemplatesCache(self):
         """See `IPOTemplateSet`."""
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this sql needs to be updated to
+        # check for the translations_usage, not official_rosetta.
         return IMasterStore(POTemplate).execute("""
             INSERT INTO SuggestivePOTemplate (
                 SELECT POTemplate.id
@@ -1437,15 +1440,13 @@
         if self.product:
             subsets = [
                 self.potemplateset.getSubset(productseries=series)
-                for series in self.product.series
-                ]
+                for series in self.product.series]
         else:
             subsets = [
                 self.potemplateset.getSubset(
                     distroseries=series,
                     sourcepackagename=self.sourcepackagename)
-                for series in self.distribution.series
-                ]
+                for series in self.distribution.series]
         for subset in subsets:
             for template in subset:
                 if name_pattern is None or re.match(name_pattern,
@@ -1548,8 +1549,7 @@
                 msgset.flags = set([
                     flag.strip()
                     for flag in row.flags_comment.split(',')
-                    if flag
-                    ])
+                    if flag])
 
             # Store the message.
             messages.append(msgset)
@@ -1578,8 +1578,9 @@
         collection = self.getTemplatesCollection()
 
         # XXX JeroenVermeulen 2010-07-15 bug=605924: Move the
-        # official_rosetta distinction into browser code.
-        if collection.target_pillar.official_rosetta:
+        # translations_usage distinction into browser code.
+        pillar = collection.target_pillar
+        if service_uses_launchpad(pillar.translations_usage):
             return collection.restrictCurrent(current_value)
         else:
             # Product/Distribution does not have translation enabled.

=== modified file 'lib/lp/translations/model/potmsgset.py'
--- lib/lp/translations/model/potmsgset.py	2010-09-06 03:35:47 +0000
+++ lib/lp/translations/model/potmsgset.py	2010-09-17 01:08:50 +0000
@@ -361,6 +361,9 @@
             query = ["(NOT %s)" % in_use_clause]
         query.append('TranslationMessage.language = %s' % sqlvalues(language))
 
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this sql needs to be updated
+        # to check for the translations_usage, not official_rosetta.
         query.append('''
             potmsgset IN (
                 SELECT POTMsgSet.id
@@ -381,7 +384,8 @@
                     POTMsgSet.id <> %s AND
                     msgid_singular = %s AND
                     POTemplate.iscurrent AND
-                    (Product.official_rosetta OR Distribution.official_rosetta)
+                    (Product.official_rosetta OR
+                        Distribution.official_rosetta)
             )''' % sqlvalues(self, self.msgid_singular))
 
         # Subquery to find the ids of TranslationMessages that are
@@ -396,8 +400,8 @@
             for form in xrange(TranslationConstants.MAX_PLURAL_FORMS)])
         ids_query_params = {
             'msgstrs': msgstrs,
-            'where': ' AND '.join(query)
-        }
+            'where': ' AND '.join(query),
+            }
         ids_query = '''
             SELECT DISTINCT ON (%(msgstrs)s)
                 TranslationMessage.id
@@ -565,8 +569,7 @@
         # plural forms.
         order.extend([
             'msgstr%s NULLS FIRST' % quote(form)
-            for form in remaining_plural_forms
-            ])
+            for form in remaining_plural_forms])
         matches = list(
             TranslationMessage.select(' AND '.join(clauses), orderBy=order))
 
@@ -722,7 +725,6 @@
         if is_imported or new_message == imported_message:
             new_message.is_imported = True
 
-
     def _isTranslationMessageASuggestion(self, force_suggestion,
                                          pofile, submitter,
                                          force_edition_rights, is_imported,
@@ -1178,4 +1180,3 @@
         """See `IPOTMsgSet`."""
         return TranslationTemplateItem.selectBy(
             potmsgset=self, orderBy=['id'])
-

=== modified file 'lib/lp/translations/model/translationsoverview.py'
--- lib/lp/translations/model/translationsoverview.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/model/translationsoverview.py	2010-09-17 01:08:50 +0000
@@ -39,13 +39,15 @@
             new_size = int(round(
                 real_minimum +
                 (size - offset - real_minimum) * multiplier))
-            normalized_sizes.append({'pillar' : pillar,
-                                     'weight' : new_size })
+            normalized_sizes.append({'pillar': pillar, 'weight': new_size})
         return normalized_sizes
 
     def getMostTranslatedPillars(self, limit=50):
         """See `ITranslationsOverview`."""
 
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this sql needs to be updated
+        # to check for the translations_usage, not official_rosetta.
         query = """
         SELECT LOWER(COALESCE(product_name, distro_name)) AS name,
                product_id,
@@ -65,7 +67,8 @@
                      distribution=distribution.id
               WHERE category=3 AND
                     (product IS NOT NULL OR distribution IS NOT NULL) AND
-                    (product.official_rosetta OR distribution.official_rosetta)
+                    (product.official_rosetta OR
+                        distribution.official_rosetta)
               GROUP BY product.displayname, product.id,
                        distribution.displayname, distribution.id
               HAVING SUM(karmavalue) > 0

=== modified file 'lib/lp/translations/model/translationsperson.py'
--- lib/lp/translations/model/translationsperson.py	2010-08-27 10:53:40 +0000
+++ lib/lp/translations/model/translationsperson.py	2010-09-17 01:08:50 +0000
@@ -262,6 +262,10 @@
         The added joins may make the overall query non-distinct, so be
         sure to enforce distinctness.
         """
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this query needs to be updated
+        # to check for the translations_usage, not official_rosetta.
+
         POTemplateJoin = Join(POTemplate, And(
             POTemplate.id == POFile.potemplateID,
             POTemplate.iscurrent == True))

=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
--- lib/lp/translations/scripts/translations_to_branch.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/scripts/translations_to_branch.py	2010-09-17 01:08:50 +0000
@@ -306,6 +306,9 @@
 
         self.store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
 
+        # XXX j.c.sackett 2010-08-30 bug=627631 Once data migration has
+        # happened for the usage enums, this sql needs to be updated to
+        # check for the translations_usage, not official_rosetta.
         product_join = Join(
             ProductSeries, Product, ProductSeries.product == Product.id)
         productseries = self.store.using(product_join).find(

=== modified file 'lib/lp/translations/stories/buildfarm/xx-build-summary.txt'
--- lib/lp/translations/stories/buildfarm/xx-build-summary.txt	2010-07-20 17:50:45 +0000
+++ lib/lp/translations/stories/buildfarm/xx-build-summary.txt	2010-09-17 01:08:50 +0000
@@ -1,9 +1,11 @@
-= TranslationTemplatesBuildJob Build Summary =
+TranslationTemplatesBuildJob Build Summary
+==========================================
 
 The builders UI can show TranslationTemplateBuildJobs, although they
 look a little different from Soyuz-style jobs.
 
-== Setup ==
+Setup
+-----
 
 Create a builder working on a TranslationTemplatesBuildJob for a branch.
 
@@ -13,6 +15,7 @@
     >>> from canonical.launchpad.interfaces.librarian import (
     ...     ILibraryFileAliasSet)
     >>> from canonical.launchpad.scripts.logger import QuietFakeLogger
+    >>> from lp.app.enums import ServiceUsage
     >>> from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
     >>> from lp.testing.factory import (
     ...     remove_security_proxy_and_shout_at_engineer)
@@ -32,7 +35,7 @@
     >>> productseries = factory.makeProductSeries(owner=owner)
     >>> product = productseries.product
     >>> naked_product = remove_security_proxy_and_shout_at_engineer(product)
-    >>> naked_product.official_rosetta = True
+    >>> naked_product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> branch = factory.makeProductBranch(product=product, owner=owner)
     >>> branch_url = branch.unique_name
 
@@ -62,7 +65,8 @@
     ...     return find_tag_by_id(browser.contents, 'current-build-summary')
 
 
-== Show summary ==
+Show summary
+------------
 
 The job's summary shows that what type of job this is.  It also links
 to the branch.

=== modified file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
--- lib/lp/translations/stories/importqueue/xx-entry-details.txt	2010-03-11 20:54:36 +0000
+++ lib/lp/translations/stories/importqueue/xx-entry-details.txt	2010-09-17 01:08:50 +0000
@@ -1,9 +1,11 @@
-= Entry details =
+Entry details
+=============
 
 The translation import queue entry page shows various details about an
 entry and its target that may be helpful in queue review.
 
     >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.app.enums import ServiceUsage
     >>> from lp.translations.model.translationimportqueue import (
     ...     TranslationImportQueue)
 
@@ -11,8 +13,8 @@
 
     >>> login(ANONYMOUS)
     >>> queue = TranslationImportQueue()
-    >>> product = factory.makeProduct()
-    >>> removeSecurityProxy(product).official_rosetta = True
+    >>> product = factory.makeProduct(
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> trunk = product.getSeries('trunk')
     >>> uploader = factory.makePerson()
     >>> entry = queue.addOrUpdateEntry(
@@ -47,7 +49,8 @@
     http://...foo.pot
 
 
-== Existing templates ==
+Existing templates
+------------------
 
 If there are translatable templates in the series, this will be stated
 and there will be a link to the templates list.
@@ -81,7 +84,8 @@
     ...
 
 
-== Source packages ==
+Source packages
+---------------
 
 The portlet shows different (well, less) information for uploads
 attached to distribution packages.

=== modified file 'lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt'
--- lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt	2009-09-02 16:35:54 +0000
+++ lib/lp/translations/stories/productseries/xx-productseries-export-to-branch.txt	2010-09-17 01:08:50 +0000
@@ -1,15 +1,16 @@
-= ProductSeries Translations Branch =
+ProductSeries Translations Branch
+=================================
 
 If a ProductSeries has its translations_branch set, Launchpad will
 periodically commit snapshots of the series' translations to that
 branch.
 
-    >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.app.enums import ServiceUsage
 
     >>> login(ANONYMOUS)
     >>> owner = factory.makePerson(email='x@xxxxxxxxxxx', password='****')
-    >>> product = factory.makeProduct(owner=owner)
-    >>> removeSecurityProxy(product).official_rosetta = True
+    >>> product = factory.makeProduct(owner=owner,
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> productseries = product.getSeries('trunk')
     >>> branch = factory.makeBranch(product=product, owner=owner)
     >>> branch_name = branch.name
@@ -99,7 +100,8 @@
     </div>
 
 
-== Disabling exports ==
+Disabling exports
+-----------------
 
 The field can also be cleared in order to disable the exports.
 
@@ -125,7 +127,8 @@
     None
 
 
-== Security ==
+Security
+--------
 
 You can only set the translations_branch to a branch that you own.
 Otherwise you'd be giving Launchpad a blanket license to commit

=== modified file 'lib/lp/translations/stories/productseries/xx-productseries-translations.txt'
--- lib/lp/translations/stories/productseries/xx-productseries-translations.txt	2010-03-31 20:25:33 +0000
+++ lib/lp/translations/stories/productseries/xx-productseries-translations.txt	2010-09-17 01:08:50 +0000
@@ -5,9 +5,10 @@
 a single product series, or instructions on how to set up a series for
 translation.
 
+    >>> from lp.app.enums import ServiceUsage
     >>> login('foo.bar@xxxxxxxxxxxxx')
-    >>> frobnicator = factory.makeProduct(name='frobnicator')
-    >>> frobnicator.official_rosetta = True
+    >>> frobnicator = factory.makeProduct(name='frobnicator',
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> frobnicator_trunk = frobnicator.getSeries('trunk')
     >>> frobnicator_trunk_url = canonical_url(
     ...     frobnicator_trunk, rootsite='translations')
@@ -148,7 +149,8 @@
 
     # Use the raw DB object to bypass the security proxy.
     >>> from lp.registry.model.product import Product
-    >>> Product.byName('bazaar').official_rosetta = False
+    >>> product = Product.byName('bazaar')
+    >>> product.translations_usage = ServiceUsage.NOT_APPLICABLE
 
 When the owner now visits the upload page for trunk, there's a notice.
 

=== modified file 'lib/lp/translations/stories/standalone/custom-language-codes.txt'
--- lib/lp/translations/stories/standalone/custom-language-codes.txt	2010-03-11 20:54:36 +0000
+++ lib/lp/translations/stories/standalone/custom-language-codes.txt	2010-09-17 01:08:50 +0000
@@ -11,11 +11,11 @@
 Custom language codes are attached to either a product or a source
 package.
 
-    >>> import re
     >>> from zope.component import getUtility
     >>> from zope.security.proxy import removeSecurityProxy
     >>> from canonical.launchpad.interfaces.launchpad import (
     ...     ILaunchpadCelebrities)
+    >>> from lp.app.enums import ServiceUsage
 
     >>> def find_custom_language_codes_link(browser):
     ...     """Find reference to custom language codes on a page."""
@@ -29,7 +29,8 @@
     ...     getUtility(ILaunchpadCelebrities).rosetta_experts)
     >>> product = factory.makeProduct(displayname="Foo", owner=owner)
     >>> trunk = product.getSeries('trunk')
-    >>> removeSecurityProxy(product).official_rosetta = True
+    >>> naked_product = removeSecurityProxy(product)
+    >>> naked_product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> template = factory.makePOTemplate(productseries=trunk)
     >>> product_page = canonical_url(product, rootsite='translations')
     >>> logout()
@@ -198,7 +199,8 @@
     >>> sourcepackagename = SourcePackageName(name='bar')
     >>> package = factory.makeSourcePackage(
     ...     sourcepackagename=sourcepackagename, distroseries=distroseries)
-    >>> removeSecurityProxy(distro).official_rosetta = True
+    >>> naked_distro = removeSecurityProxy(distro)
+    >>> naked_distro.translations_usage = ServiceUsage.LAUNCHPAD
     >>> other_series = factory.makeDistroRelease(distribution=distro)
     >>> template = factory.makePOTemplate(
     ...     distroseries=package.distroseries,

=== modified file 'lib/lp/translations/stories/standalone/xx-potemplate-edit.txt'
--- lib/lp/translations/stories/standalone/xx-potemplate-edit.txt	2010-04-22 17:07:56 +0000
+++ lib/lp/translations/stories/standalone/xx-potemplate-edit.txt	2010-09-17 01:08:50 +0000
@@ -183,9 +183,7 @@
 
   >>> referrer = 'http://translations.launchpad.dev/evolution/trunk/+templates'
   >>> admin_browser.open(referrer)
-  >>> admin_browser.getLink(
-  ...   url='/evolution/trunk/+pots/'
-  ...       'evolution-2.2/+edit').click()
+  >>> admin_browser.getLink(url='+pots/evolution-2.2/+edit').click()
   >>> admin_browser.getControl('Change').click()
   >>> admin_browser.url == referrer
   True

=== modified file 'lib/lp/translations/stories/standalone/xx-potemplate-index.txt'
--- lib/lp/translations/stories/standalone/xx-potemplate-index.txt	2010-02-16 21:21:14 +0000
+++ lib/lp/translations/stories/standalone/xx-potemplate-index.txt	2010-09-17 01:08:50 +0000
@@ -6,7 +6,7 @@
 -----------
 
 The index page for a POTemplate lists all available translations
-for a source package. No Privileges Person visits the 
+for a source package. No Privileges Person visits the
 evolution-2.2 POTemplate page.
 
     >>> anon_browser.open("http://translations.launchpad.dev/";
@@ -85,7 +85,7 @@
     </p>
 
 
-DistroSeries and ProductSeries links to related templates 
+DistroSeries and ProductSeries links to related templates
 ---------------------------------------------------------
 
 We are presented not only with links to alternate templates from the same
@@ -117,8 +117,8 @@
 A source package with five templates is created.
 
     >>> from zope.component import getUtility
-    >>> from canonical.launchpad.interfaces.launchpad import (
-    ...     ILaunchpadCelebrities)
+    >>> from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+
     >>> login('admin@xxxxxxxxxxxxx')
     >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
     >>> hoary = ubuntu.getSeries('hoary')
@@ -212,8 +212,10 @@
     >>> from zope.component import getUtility
     >>> from canonical.launchpad.interfaces.launchpad import (
     ...     ILaunchpadCelebrities)
+    >>> from lp.app.enums import ServiceUsage
     >>> login('admin@xxxxxxxxxxxxx')
-    >>> product = factory.makeProduct(name="fusa", official_rosetta=True)
+    >>> product = factory.makeProduct(name="fusa",
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> product_trunk = product.getSeries('trunk')
     >>> template = factory.makePOTemplate(
     ...     productseries=product_trunk, name='first')
@@ -253,7 +255,9 @@
 Anonymous visitors see only a list of all existing templates, with no
 administration or download/upload links.
 
-    >>> anon_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> anon_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> anon_browser.getLink('upload')
     Traceback (most recent call last):
     ...
@@ -267,7 +271,9 @@
 As an authenticated user, you should see the download link,
 but not the one for uploading file to this potemplate.
 
-    >>> user_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> user_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> user_browser.getLink('upload')
     Traceback (most recent call last):
     ...
@@ -281,27 +287,37 @@
 Beside administering this template, "Change permissions"
 and "Change details" should be also accessible.
 
-    >>> admin_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> admin_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> admin_browser.getLink('upload').click()
     >>> print admin_browser.url
     http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+upload
 
-    >>> admin_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> admin_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> admin_browser.getLink('download').click()
     >>> print admin_browser.url
     http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+export
 
-    >>> admin_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> admin_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> admin_browser.getLink('Administer this template').click()
     >>> print admin_browser.url
     http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+admin
 
-    >>> admin_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> admin_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> admin_browser.getLink('Change details').click()
     >>> print admin_browser.url
     http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+edit
 
-    >>> admin_browser.open('http://translations.launchpad.dev/ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
+    >>> admin_browser.open(
+    ...     'http://translations.launchpad.dev/'
+    ...     'ubuntu/hoary/+source/evolution/+pots/evolution-2.2')
     >>> admin_browser.getLink('Change permissions').click()
     >>> print admin_browser.url
     http://translations.launchpad.dev/ubuntu/+settings

=== modified file 'lib/lp/translations/stories/standalone/xx-product-export.txt'
--- lib/lp/translations/stories/standalone/xx-product-export.txt	2009-09-18 15:42:19 +0000
+++ lib/lp/translations/stories/standalone/xx-product-export.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,5 @@
-= Downloading Product Series Translations =
+Downloading Product Series Translations
+=======================================
 
 Products and product series that use Translations offer complete
 translation downloads.
@@ -33,16 +34,18 @@
     Your request has been received.  Expect to receive an email shortly.
 
 
-== Use of Launchpad Translations ==
+Use of Launchpad Translations
+-----------------------------
 
 The Download link is not shown if the product does not use Launchpad
 Translations.
 
     # Use the DB classes directly to avoid having to setup a zope interaction
     # (i.e. login()) and bypass the security proxy.
+    >>> from lp.app.enums import ServiceUsage
     >>> from lp.registry.model.product import Product
     >>> product = Product.byName('evolution')
-    >>> product.official_rosetta = False
+    >>> product.translations_usage = ServiceUsage.NOT_APPLICABLE
     >>> product.sync()
     >>> user_browser.open('http://translations.launchpad.dev/evolution')
     >>> user_browser.getLink('download')
@@ -51,14 +54,15 @@
     LinkNotFoundError
 
     >>> # Restore previous state for subsequent tests, and verify
-    >>> product.official_rosetta = True
+    >>> product.translations_usage = ServiceUsage.LAUNCHPAD
     >>> product.sync()
     >>> user_browser.open('http://translations.launchpad.dev/evolution')
     >>> user_browser.getLink('download') is not None
     True
 
 
-== Authorization ==
+Authorization
+-------------
 
 Only logged-in users get the option to request downloads.
 

=== modified file 'lib/lp/translations/stories/standalone/xx-product-translations.txt'
--- lib/lp/translations/stories/standalone/xx-product-translations.txt	2010-08-25 00:01:57 +0000
+++ lib/lp/translations/stories/standalone/xx-product-translations.txt	2010-09-17 01:08:50 +0000
@@ -1,4 +1,3 @@
-====================
 Product Translations
 ====================
 

=== modified file 'lib/lp/translations/stories/standalone/xx-series-templates.txt'
--- lib/lp/translations/stories/standalone/xx-series-templates.txt	2010-07-15 13:57:23 +0000
+++ lib/lp/translations/stories/standalone/xx-series-templates.txt	2010-09-17 01:08:50 +0000
@@ -85,16 +85,14 @@
     >>> utc_browser.open(
     ...     'http://translations.launchpad.dev/ubuntu/hoary/+templates')
     >>> utc_browser.getLink(
-    ...     url='ubuntu/hoary/+source/evolution/'
-    ...         '+pots/evolution-2.2/+edit').click()
+    ...     url='+source/evolution/+pots/evolution-2.2/+edit').click()
     >>> print utc_browser.url
     http://.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+edit
 
     >>> utc_browser.open(
     ...     'http://translations.launchpad.dev/ubuntu/hoary/+templates')
     >>> utc_browser.getLink(
-    ...     url='/ubuntu/hoary/+source/evolution/'
-    ...         '+pots/evolution-2.2/+admin').click()
+    ...     url='+source/evolution/+pots/evolution-2.2/+admin').click()
     >>> print utc_browser.url
     http://.../ubuntu/hoary/+source/evolution/+pots/evolution-2.2/+admin
 
@@ -104,16 +102,14 @@
     >>> utc_browser.open(
     ...     'http://translations.launchpad.dev/ubuntu/hoary/+templates')
     >>> utc_browser.getLink(
-    ...     url='ubuntu/hoary/+source/evolution/'
-    ...         '+pots/disabled-template/+edit').click()
+    ...     url='+source/evolution/+pots/disabled-template/+edit').click()
     >>> print utc_browser.url
     http://.../ubuntu/hoary/+source/evolution/+pots/disabled-template/+edit
 
     >>> utc_browser.open(
     ...     'http://translations.launchpad.dev/ubuntu/hoary/+templates')
     >>> utc_browser.getLink(
-    ...     url='/ubuntu/hoary/+source/evolution/'
-    ...         '+pots/disabled-template/+admin').click()
+    ...     url='+source/evolution/+pots/disabled-template/+admin').click()
     >>> print utc_browser.url
     http://.../ubuntu/hoary/+source/evolution/+pots/disabled-template/+admin
 

=== modified file 'lib/lp/translations/stories/standalone/xx-template-description-escaping.txt'
--- lib/lp/translations/stories/standalone/xx-template-description-escaping.txt	2009-09-14 15:41:21 +0000
+++ lib/lp/translations/stories/standalone/xx-template-description-escaping.txt	2010-09-17 01:08:50 +0000
@@ -1,12 +1,14 @@
     >>> import re
     >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.app.enums import ServiceUsage
 
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> package = factory.makeSourcePackage()
     >>> template = removeSecurityProxy(factory.makePOTemplate(
     ...     distroseries=package.distroseries,
     ...     sourcepackagename=package.sourcepackagename))
-    >>> template.distroseries.distribution.official_rosetta = True
+    >>> distribution = template.distroseries.distribution
+    >>> distribution.translations_usage = ServiceUsage.LAUNCHPAD
     >>> template.distroseries.hide_all_translations = False
     >>> template.description = "See http://example.com/ for an example!"
     >>> package_url = canonical_url(package, rootsite='translations')

=== modified file 'lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt'
--- lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt	2010-01-20 20:21:42 +0000
+++ lib/lp/translations/stories/translationfocus/xx-product-translationfocus.txt	2010-09-17 01:08:50 +0000
@@ -1,9 +1,10 @@
 The translation focus of a product can be explicitly set to a specific series.
 When not set, launchpad recommends the development focus to translate.
 
+    >>> from lp.app.enums import ServiceUsage
     >>> login('admin@xxxxxxxxxxxxx')
     >>> fooproject = factory.makeProduct(name="fooproject")
-    >>> fooproject.official_rosetta = True
+    >>> fooproject.translations_usage = ServiceUsage.LAUNCHPAD
     >>> fooproject_trunk = fooproject.getSeries("trunk")
     >>> fooproject_url = canonical_url(
     ...     fooproject, rootsite="translations")
@@ -25,7 +26,8 @@
     ...
     IndexError: list index out of range
 
-== Setting the translation focus ==
+Setting the translation focus
+=============================
 
     >>> login('admin@xxxxxxxxxxxxx')
     >>> from zope.security.proxy import removeSecurityProxy
@@ -35,7 +37,7 @@
     >>> pofile = factory.makePOFile("pt_BR", potemplate=pot_main)
     >>> logout()
 
-When the translation focus is not set, Launchpad suggests the 
+When the translation focus is not set, Launchpad suggests the
 development focus as the current series to be translated.
 It needs to be translatable.
 

=== modified file 'lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt'
--- lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt	2010-02-16 21:21:14 +0000
+++ lib/lp/translations/stories/translationgroups/xx-change-translation-policy.txt	2010-09-17 01:08:50 +0000
@@ -4,11 +4,15 @@
 A product owner, Rosetta expert, and Ubuntu translations coordinator
 browser is created.
 
+    >>> from lp.app.enums import ServiceUsage
+
     >>> login('admin@xxxxxxxxxxxxx')
     >>> product_owner = factory.makePerson(
     ...     email="po@xxxxxx", password="test")
     >>> chestii = factory.makeProduct(
-    ...     name='chestii', owner=product_owner, official_rosetta=True)
+    ...     name='chestii',
+    ...     owner=product_owner,
+    ...     translations_usage=ServiceUsage.LAUNCHPAD)
     >>> logout()
     >>> dtc_browser = setupDTCBrowser()
     >>> re_browser = setupRosettaExpertBrowser()

=== modified file 'lib/lp/translations/templates/object-templates.pt'
--- lib/lp/translations/templates/object-templates.pt	2010-07-15 09:41:27 +0000
+++ lib/lp/translations/templates/object-templates.pt	2010-09-17 01:08:50 +0000
@@ -73,80 +73,13 @@
       <div class="yui-b">
       <table class="sortable listing" id="templates_table">
         <thead>
-          <tr>
-            <th class="priority_column">Priority</th>
-            <th tal:condition="view/is_distroseries"
-                class="sourcepackage_column">Source package</th>
-            <th class="template_column">Template name</th>
-            <th class="length_column">Length</th>
-            <th class="lastupdate_column">Updated</th>
-            <th class="actions_column"
-                tal:condition="context/required:launchpad.AnyPerson">
-              Actions</th>
+          <tr tal:content="structure view/renderTemplatesHeader">
           </tr>
         </thead>
         <tbody>
-          <tal:templates repeat="template view/iter_templates">
-            <tr tal:define="
-                  inactive_css_class python:view.rowCSSClass(template);
-                  template_url template/fmt:url"
-                tal:condition="python: view.can_edit or template.iscurrent"
-                tal:attributes="
-                  class string: template_row ${inactive_css_class}">
-              <td class="priority_column"
-                  tal:content="template/priority">7</td>
-              <td tal:condition="view/is_distroseries"
-                  tal:content="template/sourcepackagename/name"
-                  class="sourcepackage_column">Source package
-              </td>
-              <td class="template_column">
-                <a tal:attributes="href template_url">
-                  <span tal:content="template/name">Template name</span>
-                </a>
-                <tal:inactive condition="not: template/iscurrent">
-                  (inactive)
-                </tal:inactive>
-              </td>
-              <td class="length_column"
-                  tal:content="template/messagecount">1777</td>
-              <td class="lastupdate_column">
-                <span class="sortkey"
-                      tal:condition="template/date_last_updated"
-                      tal:content="template/date_last_updated/fmt:datetime">
-                      time sort key
-                </span>
-                <span class="lastupdate_column"
-                      tal:condition="template/date_last_updated"
-                      tal:attributes="
-                        title template/date_last_updated/fmt:datetime"
-                      tal:content="
-                        template/date_last_updated/fmt:approximatedate"
-                >
-                   2009-09-23
-                </span>
-              </td>
-              <td class="actions_column"
-                  tal:condition="context/required:launchpad.AnyPerson">
-              <div class="template_links">
-                <a class="sprite edit"
-                    tal:condition="view/can_edit"
-                    tal:attributes="
-                        href string: ${template_url}/+edit">Edit</a>
-                <a class="sprite add"
-                    tal:condition="view/can_edit"
-                    tal:attributes="
-                        href string: ${template_url}/+upload">Upload</a>
-                <a class="sprite download"
-                    tal:attributes="
-                        href string: ${template_url}/+export">Download</a>
-                <a class="sprite edit"
-                    tal:condition="view/can_admin"
-                    tal:attributes="
-                        href string: ${template_url}/+admin">Administer</a>
-              </div>
-              </td>
-            </tr>
-          </tal:templates>
+          <tal:templates
+              repeat="template view/iter_templates"
+              content="structure python:view.renderTemplateRow(template)"/>
         </tbody>
       </table>
       </div>

=== modified file 'lib/lp/translations/templates/product-portlet-not-using-launchpad.pt'
--- lib/lp/translations/templates/product-portlet-not-using-launchpad.pt	2010-03-15 20:17:21 +0000
+++ lib/lp/translations/templates/product-portlet-not-using-launchpad.pt	2010-09-17 01:08:50 +0000
@@ -5,7 +5,7 @@
   omit-tag="">
 
     <div id="not-translated-in-launchpad"
-         tal:condition="not: context/official_rosetta">
+         tal:condition="not: context/translations_usage/enumvalue:LAUNCHPAD">
       <strong>
         This project is not using Launchpad for translations.
       </strong>

=== modified file 'lib/lp/translations/templates/productseries-translations.pt'
--- lib/lp/translations/templates/productseries-translations.pt	2009-12-16 15:21:36 +0000
+++ lib/lp/translations/templates/productseries-translations.pt	2010-09-17 01:08:50 +0000
@@ -30,7 +30,7 @@
           <a href="https://help.launchpad.net/Translations/YourProject";>start
             translating your project</a>,
           <tal:uses-translations condition="not:
-                                            context/product/official_rosetta">
+                    context/product/translations_usage/enumvalue:LAUNCHPAD">
             you should enable translations in your project settings, and
           </tal:uses-translations>
           you can either

=== modified file 'lib/lp/translations/tests/test_autoapproval.py'
--- lib/lp/translations/tests/test_autoapproval.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_autoapproval.py	2010-09-17 01:08:50 +0000
@@ -23,6 +23,7 @@
 from canonical.launchpad.interfaces.lpstorm import IMasterStore
 from canonical.launchpad.webapp.testing import verifyObject
 from canonical.testing import LaunchpadZopelessLayer
+from lp.app.enums import ServiceUsage
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.sourcepackagename import (
@@ -714,8 +715,8 @@
         # _get_pofile_from_language will find an enabled template, and
         # return either an existing POFile for the given language, or a
         # newly created one.
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         trunk = product.getSeries('trunk')
         template = self.factory.makePOTemplate(
             productseries=trunk, translation_domain='domain')
@@ -732,8 +733,8 @@
         # _get_pofile_from_language will not consider a disabled
         # template as an auto-approval target, and so will not return a
         # POFile for it.
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         trunk = product.getSeries('trunk')
         template = self.factory.makePOTemplate(
             productseries=trunk, translation_domain='domain')
@@ -750,8 +751,8 @@
         # When the template has translation credits, a new dummy translation
         # is created in the new POFile. Since this is running with gardener
         # privileges, we need to check that this works, too.
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         trunk = product.getSeries('trunk')
         template = self.factory.makePOTemplate(
             productseries=trunk, translation_domain='domain')
@@ -778,8 +779,8 @@
 
     def _makeProductEntry(self, path='foo.pot', status=None):
         """Simulate upload for a product."""
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         trunk = product.getSeries('trunk')
         entry = self.queue.addOrUpdateEntry(
             path, '# contents', False, product.owner, productseries=trunk)

=== modified file 'lib/lp/translations/tests/test_empty_messages.py'
--- lib/lp/translations/tests/test_empty_messages.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_empty_messages.py	2010-09-17 01:08:50 +0000
@@ -4,17 +4,17 @@
 __metaclass__ = type
 
 from datetime import datetime
-import unittest
 
 from pytz import timezone
 from zope.component import getUtility
 
 from canonical.testing import LaunchpadZopelessLayer
+from lp.app.enums import ServiceUsage
 from lp.services.worlddata.interfaces.language import ILanguageSet
-from lp.testing.factory import LaunchpadObjectFactory
-
-
-class TestTranslationEmptyMessages(unittest.TestCase):
+from lp.testing import TestCaseWithFactory
+
+
+class TestTranslationEmptyMessages(TestCaseWithFactory):
     """Test behaviour of empty translation messages."""
 
     layer = LaunchpadZopelessLayer
@@ -23,13 +23,15 @@
         """Set up context to test in."""
         # Pretend we have a product being translated to Serbian.
         # This is where we are going to be importing translations to.
-        factory = LaunchpadObjectFactory()
-        self.factory = factory
-        self.productseries = factory.makeProductSeries()
-        self.productseries.product.official_rosetta = True
-        self.potemplate = factory.makePOTemplate(self.productseries)
+        super(TestTranslationEmptyMessages, self).setUp()
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        self.productseries = self.factory.makeProductSeries(product=product)
+        self.potemplate = self.factory.makePOTemplate(self.productseries)
         self.serbian = getUtility(ILanguageSet).getLanguageByCode('sr')
-        self.pofile_sr = factory.makePOFile('sr', potemplate=self.potemplate)
+        self.pofile_sr = self.factory.makePOFile(
+            'sr',
+            potemplate=self.potemplate)
         self.now = datetime.now(timezone('UTC'))
 
     def test_NoEmptyImporedTranslation(self):

=== modified file 'lib/lp/translations/tests/test_hastranslationtemplates.py'
--- lib/lp/translations/tests/test_hastranslationtemplates.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_hastranslationtemplates.py	2010-09-17 01:08:50 +0000
@@ -6,6 +6,7 @@
 from zope.interface.verify import verifyObject
 
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.testing import TestCaseWithFactory
 from lp.translations.interfaces.potemplate import IHasTranslationTemplates
 from lp.translations.interfaces.translationfileformat import (
@@ -152,7 +153,7 @@
 
         # A product or distribution that doesn't use Launchpad for
         # translations has no current templates.
-        self.product_or_distro.official_rosetta = False
+        self.product_or_distro.translations_usage = ServiceUsage.EXTERNAL
         self.assertFalse(self.container.has_current_translation_templates)
 
     def test_getTranslationTemplateFormats(self):
@@ -205,7 +206,7 @@
         super(TestProductSeriesHasTranslationTemplates, self).setUp()
         self.container = self.factory.makeProductSeries()
         self.product_or_distro = self.container.product
-        self.product_or_distro.official_rosetta = True
+        self.product_or_distro.translations_usage = ServiceUsage.LAUNCHPAD
 
 
 class TestSourcePackageHasTranslationTemplates(
@@ -223,7 +224,7 @@
         super(TestSourcePackageHasTranslationTemplates, self).setUp()
         self.container = self.factory.makeSourcePackage()
         self.product_or_distro = self.container.distroseries.distribution
-        self.product_or_distro.official_rosetta = True
+        self.product_or_distro.translations_usage = ServiceUsage.LAUNCHPAD
 
 
 class TestDistroSeriesHasTranslationTemplates(
@@ -243,4 +244,4 @@
         super(TestDistroSeriesHasTranslationTemplates, self).setUp()
         self.container = self.factory.makeDistroRelease()
         self.product_or_distro = self.container.distribution
-        self.product_or_distro.official_rosetta = True
+        self.product_or_distro.translations_usage = ServiceUsage.LAUNCHPAD

=== modified file 'lib/lp/translations/tests/test_pofile.py'
--- lib/lp/translations/tests/test_pofile.py	2010-09-03 14:38:20 +0000
+++ lib/lp/translations/tests/test_pofile.py	2010-09-17 01:08:50 +0000
@@ -24,6 +24,7 @@
     LaunchpadZopelessLayer,
     ZopelessDatabaseLayer,
     )
+from lp.app.enums import ServiceUsage
 from lp.testing import TestCaseWithFactory
 from lp.translations.interfaces.pofile import IPOFileSet
 from lp.translations.interfaces.translatablemessage import (
@@ -46,12 +47,13 @@
         # Create a product with two series and a shared POTemplate
         # in different series ('devel' and 'stable').
         super(TestTranslationSharedPOFile, self).setUp()
-        self.foo = self.factory.makeProduct(name='foo')
+        self.foo = self.factory.makeProduct(
+            name='foo',
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_devel = self.factory.makeProductSeries(
             name='devel', product=self.foo)
         self.foo_stable = self.factory.makeProductSeries(
             name='stable', product=self.foo)
-        self.foo.official_rosetta = True
 
         # POTemplate is 'shared' if it has the same name ('messages').
         self.devel_potemplate = self.factory.makePOTemplate(
@@ -76,7 +78,8 @@
             'http://translations.launchpad.dev/foo/devel/+pots/messages/sr',
             canonical_url(self.devel_sr_pofile))
         self.assertEqual(
-            'http://translations.launchpad.dev/foo/devel/+pots/messages/sr/+details',
+            ('http://translations.launchpad.dev/'
+            'foo/devel/+pots/messages/sr/+details'),
             canonical_url(self.devel_sr_pofile, view_name="+details"))
 
     def test_findPOTMsgSetsContaining(self):
@@ -879,12 +882,12 @@
         # Create a product with two series and a shared POTemplate
         # in different series ('devel' and 'stable').
         super(TestSharedPOFileCreation, self).setUp()
-        self.foo = self.factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_devel = self.factory.makeProductSeries(
             name='devel', product=self.foo)
         self.foo_stable = self.factory.makeProductSeries(
             name='stable', product=self.foo)
-        self.foo.official_rosetta = True
 
     def test_pofile_creation_shared(self):
         # When a pofile is created in a POTemplate it is also created in
@@ -1009,12 +1012,12 @@
         # Create a product with two series and a shared POTemplate
         # in different series ('devel' and 'stable').
         super(TestTranslationPOFilePOTMsgSetOrdering, self).setUp()
-        self.foo = self.factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_devel = self.factory.makeProductSeries(
             name='devel', product=self.foo)
         self.foo_stable = self.factory.makeProductSeries(
             name='stable', product=self.foo)
-        self.foo.official_rosetta = True
 
         # POTemplate is 'shared' if it has the same name ('messages').
         self.devel_potemplate = self.factory.makePOTemplate(
@@ -1288,8 +1291,8 @@
         # We create a product with two series, and attach
         # a POTemplate and Serbian POFile to each, making
         # sure they share translations (potemplates have the same name).
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         series1 = self.factory.makeProductSeries(product=product,
                                                  name='one')
         series2 = self.factory.makeProductSeries(product=product,
@@ -1324,8 +1327,8 @@
         # This is a test for bug #414832 which caused sharing POFiles
         # of the touched POFile not to be returned if they had
         # IDs smaller than the touched POFile.
-        product = self.factory.makeProduct()
-        product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         series1 = self.factory.makeProductSeries(product=product,
                                                  name='one')
         series2 = self.factory.makeProductSeries(product=product,
@@ -1357,7 +1360,7 @@
         # POFile to each, making sure they share translations
         # (potemplates have the same name).
         distro = self.factory.makeDistribution()
-        distro.official_rosetta = True
+        distro.translations_usage = ServiceUsage.LAUNCHPAD
         series1 = self.factory.makeDistroRelease(distribution=distro,
                                                  name='one')
         sourcepackagename = self.factory.makeSourcePackageName()
@@ -1395,9 +1398,9 @@
         # Make sure POFiles which are in different products
         # are not returned even though they have the same potemplate name.
         series1 = self.factory.makeProductSeries(name='one')
-        series1.product.official_rosetta = True
+        series1.product.translations_usage = ServiceUsage.LAUNCHPAD
         series2 = self.factory.makeProductSeries(name='two')
-        series2.product.official_rosetta = True
+        series1.product.translations_usage = ServiceUsage.LAUNCHPAD
         self.assertNotEqual(series1.product, series2.product)
 
         potemplate1 = self.factory.makePOTemplate(name='shared',

=== modified file 'lib/lp/translations/tests/test_potemplate.py'
--- lib/lp/translations/tests/test_potemplate.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_potemplate.py	2010-09-17 01:08:50 +0000
@@ -7,6 +7,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing import DatabaseFunctionalLayer
+from lp.app.enums import ServiceUsage
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.testing import TestCaseWithFactory
@@ -327,8 +328,8 @@
 
     def setUp(self):
         super(TestTemplatePrecedence, self).setUp(user='mark@xxxxxxxxxxx')
-        self.product = self.factory.makeProduct()
-        self.product.official_rosetta = True
+        self.product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.trunk = self.product.getSeries('trunk')
         self.one_dot_oh = self.factory.makeProductSeries(
             product=self.product, name='one')

=== modified file 'lib/lp/translations/tests/test_potmsgset.py'
--- lib/lp/translations/tests/test_potmsgset.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_potmsgset.py	2010-09-17 01:08:50 +0000
@@ -20,6 +20,7 @@
 
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import IProductSet
 from lp.services.worlddata.interfaces.language import ILanguageSet
@@ -45,12 +46,12 @@
         # Create a product with two series and a shared POTemplate
         # in different series ('devel' and 'stable').
         super(TestTranslationSharedPOTMsgSets, self).setUp()
-        self.foo = self.factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_devel = self.factory.makeProductSeries(
             name='devel', product=self.foo)
         self.foo_stable = self.factory.makeProductSeries(
             name='stable', product=self.foo)
-        self.foo.official_rosetta = True
 
         # POTemplate is 'shared' if it has the same name ('messages').
         self.devel_potemplate = self.factory.makePOTemplate(
@@ -324,7 +325,8 @@
         # Create an external POTemplate with a POTMsgSet using
         # the same English string as the one in self.potmsgset.
         external_template = self.factory.makePOTemplate()
-        external_template.productseries.product.official_rosetta = True
+        product = external_template.productseries.product
+        product.translations_usage = ServiceUsage.LAUNCHPAD
         external_potmsgset = self.factory.makePOTMsgSet(
             external_template,
             singular=self.potmsgset.singular_text)
@@ -383,7 +385,8 @@
         # Create an external POTemplate with a POTMsgSet using
         # the same English string as the one in self.potmsgset.
         external_template = self.factory.makePOTemplate()
-        external_template.productseries.product.official_rosetta = True
+        product = external_template.productseries.product
+        product.translations_usage = ServiceUsage.LAUNCHPAD
         external_potmsgset = self.factory.makePOTMsgSet(
             external_template,
             singular=self.potmsgset.singular_text)
@@ -738,10 +741,10 @@
         # create TranslationMessage objects.
         super(TestPOTMsgSetSuggestions, self).setUp()
         self.now = self.gen_now().next
-        self.foo = self.factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_main = self.factory.makeProductSeries(
             name='main', product=self.foo)
-        self.foo.official_rosetta = True
 
         self.potemplate = self.factory.makePOTemplate(
             productseries=self.foo_main, name="messages")
@@ -926,10 +929,10 @@
         # create TranslationMessage objects.
         super(TestPOTMsgSetResetTranslation, self).setUp()
         self.now = self.gen_now().next
-        self.foo = self.factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_main = self.factory.makeProductSeries(
             name='main', product=self.foo)
-        self.foo.official_rosetta = True
 
         self.potemplate = self.factory.makePOTemplate(
             productseries=self.foo_main, name="messages")

=== modified file 'lib/lp/translations/tests/test_productserieslanguage.py'
--- lib/lp/translations/tests/test_productserieslanguage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_productserieslanguage.py	2010-09-17 01:08:50 +0000
@@ -10,6 +10,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.testing import TestCaseWithFactory
 from lp.translations.interfaces.productserieslanguage import (
@@ -26,8 +27,10 @@
     def setUp(self):
         # Create a productseries that uses translations.
         TestCaseWithFactory.setUp(self)
-        self.productseries = self.factory.makeProductSeries()
-        self.productseries.product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        self.productseries = self.factory.makeProductSeries(
+            product=product)
 
     def test_no_templates_no_translation(self):
         # There are no templates and no translations.
@@ -125,8 +128,10 @@
     def setUp(self):
         # Create a productseries that uses translations.
         TestCaseWithFactory.setUp(self)
-        self.productseries = self.factory.makeProductSeries()
-        self.productseries.product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        self.productseries = self.factory.makeProductSeries(
+            product=product)
         self.psl_set = getUtility(IProductSeriesLanguageSet)
         self.language = getUtility(ILanguageSet).getLanguageByCode('sr')
 

=== modified file 'lib/lp/translations/tests/test_shared_potemplate.py'
--- lib/lp/translations/tests/test_shared_potemplate.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_shared_potemplate.py	2010-09-17 01:08:50 +0000
@@ -10,6 +10,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.testing.factory import LaunchpadObjectFactory
 
 
@@ -24,12 +25,12 @@
         # in different series ('devel' and 'stable').
         factory = LaunchpadObjectFactory()
         self.factory = factory
-        self.foo = factory.makeProduct()
+        self.foo = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.foo_devel = factory.makeProductSeries(
             name='devel', product=self.foo)
         self.foo_stable = factory.makeProductSeries(
             name='stable', product=self.foo)
-        self.foo.official_rosetta = True
 
         # POTemplate is a 'sharing' one if it has the same name ('messages').
         self.devel_potemplate = factory.makePOTemplate(

=== modified file 'lib/lp/translations/tests/test_suggestions.py'
--- lib/lp/translations/tests/test_suggestions.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_suggestions.py	2010-09-17 01:08:50 +0000
@@ -17,6 +17,7 @@
 
 from canonical.config import config
 from canonical.testing import LaunchpadZopelessLayer
+from lp.app.enums import ServiceUsage
 from lp.services.worlddata.interfaces.language import ILanguageSet
 from lp.testing.factory import LaunchpadObjectFactory
 from lp.translations.interfaces.translationmessage import (
@@ -36,10 +37,14 @@
         # suggestions for the other.
         factory = LaunchpadObjectFactory()
         self.factory = factory
-        self.foo_trunk = factory.makeProductSeries()
-        self.bar_trunk = factory.makeProductSeries()
-        self.foo_trunk.product.official_rosetta = True
-        self.bar_trunk.product.official_rosetta = True
+        foo_product = factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        bar_product = factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        self.foo_trunk = factory.makeProductSeries(
+            product=foo_product)
+        self.bar_trunk = factory.makeProductSeries(
+            product=bar_product)
         self.foo_template = factory.makePOTemplate(self.foo_trunk)
         self.bar_template = factory.makePOTemplate(self.bar_trunk)
         self.nl = getUtility(ILanguageSet).getLanguageByCode('nl')
@@ -222,5 +227,6 @@
                           "TranslationMessage with errors is not correctly"
                           "marked as such in the database.")
 
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/translations/tests/test_translatablemessage.py'
--- lib/lp/translations/tests/test_translatablemessage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_translatablemessage.py	2010-09-17 01:08:50 +0000
@@ -15,6 +15,7 @@
 import transaction
 
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.testing import TestCaseWithFactory
 from lp.translations.model.translatablemessage import TranslatableMessage
 
@@ -31,8 +32,8 @@
         `POTMsgSet`, as well as a Esperanto translation.
         """
         super(TestTranslatableMessageBase, self).setUp()
-        self.product = self.factory.makeProduct()
-        self.product.official_rosetta = True
+        self.product = self.factory.makeProduct(
+            translations_usage=ServiceUsage.LAUNCHPAD)
         self.trunk = self.product.getSeries('trunk')
         self.potemplate = self.factory.makePOTemplate(
             productseries=self.trunk)
@@ -137,7 +138,8 @@
         super(TestTranslatableMessageExternal, self).setUp()
         common_msgid = self.potmsgset.singular_text
         self.external_potemplate = self.factory.makePOTemplate()
-        self.external_potemplate.productseries.product.official_rosetta = True
+        product = self.external_potemplate.productseries.product
+        product.translations_usage = ServiceUsage.LAUNCHPAD
         self.external_potmsgset = self.factory.makePOTMsgSet(
             potemplate=self.external_potemplate,
             singular=common_msgid, sequence=1)
@@ -175,24 +177,24 @@
         super(TestTranslatableMessageSuggestions, self).setUp()
         self.now = self.gen_now().next
         self.suggestion1 = self._createTranslation(date_updated=self.now())
-        self.current =  self._createTranslation(is_current=True,
+        self.current = self._createTranslation(is_current=True,
                                                 date_updated=self.now())
         self.suggestion2 = self._createTranslation(date_updated=self.now())
         self.message = TranslatableMessage(self.potmsgset, self.pofile)
 
     def test_getAllSuggestions(self):
-        # There are three different methods to return 
+        # There are three different methods to return
         suggestions = self.message.getAllSuggestions()
         self.assertContentEqual([self.suggestion1, self.suggestion2],
                                 suggestions)
 
     def test_getDismissedSuggestions(self):
-        # There are three different methods to return 
+        # There are three different methods to return
         suggestions = self.message.getDismissedSuggestions()
         self.assertContentEqual([self.suggestion1], suggestions)
 
     def test_getUnreviewedSuggestions(self):
-        # There are three different methods to return 
+        # There are three different methods to return
         suggestions = self.message.getUnreviewedSuggestions()
         self.assertContentEqual([self.suggestion2], suggestions)
 

=== modified file 'lib/lp/translations/tests/test_translatedlanguage.py'
--- lib/lp/translations/tests/test_translatedlanguage.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_translatedlanguage.py	2010-09-17 01:08:50 +0000
@@ -8,6 +8,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing import ZopelessDatabaseLayer
+from lp.app.enums import ServiceUsage
 from lp.testing import TestCaseWithFactory
 from lp.translations.interfaces.productserieslanguage import (
     IProductSeriesLanguageSet,
@@ -24,8 +25,10 @@
     def setUp(self):
         # Create a productseries that uses translations.
         TestCaseWithFactory.setUp(self)
-        self.productseries = self.factory.makeProductSeries()
-        self.productseries.product.official_rosetta = True
+        product = self.factory.makeProduct(
+            translations_usage = ServiceUsage.LAUNCHPAD)
+        self.productseries = self.factory.makeProductSeries(
+            product=product)
         self.parent = self.productseries
         self.psl_set = getUtility(IProductSeriesLanguageSet)
         self.language = self.factory.makeLanguage('sr@test')

=== modified file 'lib/lp/translations/tests/test_translations_to_review.py'
--- lib/lp/translations/tests/test_translations_to_review.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/tests/test_translations_to_review.py	2010-09-17 01:08:50 +0000
@@ -16,6 +16,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.testing import DatabaseFunctionalLayer
+from lp.app.enums import ServiceUsage
 from lp.services.worlddata.model.language import LanguageSet
 from lp.testing import TestCaseWithFactory
 from lp.translations.interfaces.translationsperson import ITranslationsPerson
@@ -28,6 +29,7 @@
 
 class ReviewTestMixin:
     """Base for testing which translations a reviewer can review."""
+
     def setUpMixin(self, for_product=True):
         """Set up test environment.
 
@@ -68,7 +70,7 @@
         transaction.commit()
 
         self.supercontext.translationgroup = self.translationgroup
-        self.supercontext.official_rosetta = True
+        self.supercontext.translations_usage = ServiceUsage.LAUNCHPAD
 
         self.potemplate = self.factory.makePOTemplate(
             productseries=self.productseries, distroseries=self.distroseries,
@@ -111,6 +113,7 @@
 
     Can be applied to product or distribution setups.
     """
+
     def test_OneFileToReview(self):
         # In the base case, the method finds one POFile for self.person
         # to review.
@@ -131,7 +134,7 @@
     def test_getReviewableTranslationFiles_not_translating_in_launchpad(self):
         # We don't see products/distros that don't use Launchpad for
         # translations.
-        self.supercontext.official_rosetta = False
+        self.supercontext.translations_usage = ServiceUsage.NOT_APPLICABLE
         self.assertEqual(self._getReviewables(), [])
 
     def test_getReviewableTranslationFiles_non_reviewer(self):
@@ -200,7 +203,7 @@
         other_pofile = removeSecurityProxy(other_pofile)
 
         product = other_pofile.potemplate.productseries.product
-        product.official_rosetta = True
+        product.translations_usage = ServiceUsage.LAUNCHPAD
 
         if with_unreviewed:
             other_pofile.unreviewed_count = 1

=== modified file 'lib/lp/translations/windmill/tests/test_documentation_links.py'
--- lib/lp/translations/windmill/tests/test_documentation_links.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/windmill/tests/test_documentation_links.py	2010-09-17 01:08:50 +0000
@@ -9,6 +9,7 @@
 from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.windmill.testing import lpuser
+from lp.app.enums import ServiceUsage
 from lp.testing import WindmillTestCase
 from lp.translations.windmill.testing import TranslationsWindmillLayer
 
@@ -52,7 +53,7 @@
         project = self.factory.makeProduct(
             name='test-product',
             displayname='Test Product',
-            official_rosetta=True)
+            translations_usage=ServiceUsage.LAUNCHPAD)
         removeSecurityProxy(project).translationgroup = group
 
         potemplate = self.createPOTemplateWithPOTMsgSets(

=== modified file 'lib/lp/translations/windmill/tests/test_import_queue.py'
--- lib/lp/translations/windmill/tests/test_import_queue.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/windmill/tests/test_import_queue.py	2010-09-17 01:08:50 +0000
@@ -8,7 +8,6 @@
 
 import transaction
 from zope.component import getUtility
-from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.webapp import canonical_url
 from canonical.launchpad.windmill.testing import lpuser
@@ -18,6 +17,7 @@
     SLEEP,
     )
 from canonical.launchpad.windmill.testing.lpuser import login_person
+from lp.app.enums import ServiceUsage
 from lp.testing import WindmillTestCase
 from lp.translations.interfaces.translationimportqueue import (
     ITranslationImportQueue,
@@ -41,8 +41,8 @@
             'field.potemplate',
             'field.potemplate_name',
             'field.language',
-            ]
-    }
+            ],
+        }
     SELECT_FIELDS = [
         'field.potemplate',
         'field.language',
@@ -55,8 +55,7 @@
             input_tag = 'input'
         return (
             u"//tr[contains(@class,'unseen')]"
-            u"//%s[@id='%s']" % (input_tag, field_id)
-                )
+            u"//%s[@id='%s']" % (input_tag, field_id))
 
     def _assertAllFieldsVisible(self, client, fields):
         """Assert that all given fields are visible.
@@ -279,8 +278,9 @@
             name="hubert", displayname="Hubert Hunt", password="test",
             email="hubert@xxxxxxxxxxx")
         # Create a project and an import entry with it.
-        product = self.factory.makeProduct(owner=hubert)
-        removeSecurityProxy(product).official_rosetta = True
+        product = self.factory.makeProduct(
+            owner=hubert,
+            translations_usage=ServiceUsage.LAUNCHPAD)
         productseries = product.getSeries('trunk')
         queue = getUtility(ITranslationImportQueue)
         potemplate = self.factory.makePOTemplate(productseries=productseries)

=== modified file 'utilities/sourcedeps.conf'
--- utilities/sourcedeps.conf	2010-09-03 03:12:39 +0000
+++ utilities/sourcedeps.conf	2010-09-17 01:08:50 +0000
@@ -1,4 +1,4 @@
-bzr-builder lp:~launchpad-pqm/bzr-builder/trunk;revno=65
+bzr-builder lp:~launchpad-pqm/bzr-builder/trunk;revno=66
 bzr-git lp:~launchpad-pqm/bzr-git/devel;revno=258
 bzr-hg lp:~launchpad-pqm/bzr-hg/devel;revno=283
 bzr-loom lp:~launchpad-pqm/bzr-loom/trunk;revno=48


Follow ups