← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/export-translation-api into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/export-translation-api into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/export-translation-api/+merge/52257

= Summary ==
Permit translation configuration via JavaScript

== Proposed fix ==
Export the following over the web service:
IProduct.translation_permission
IProduct.translation_group
IProduct.translations_usage
IProductSeries.translations_autoimport_mode
ISourcePackage.setPackaging
ISourcePackage.deletePackaging
ITranslationGroup.name
ITranslationGroup.title
ITranslationGroupSet.getByName


== Pre-implementation notes ==
Discussed with henninge

== Implementation details ==
Implemented WebServiceTestCase to simplify writing test cases.  Made ws_object's URL handling somewhat cleaner by specifying the correct root
URL.

Removed obsolete comment by Mark-- SQLObject is now irrelevant, and we do not discourage accessing objects by id.

Drive-by cleanup of xx-change-assignee.txt.  At first, I was just fixing the spelling of "tries".  That'll learn me :-)

Had to implement ITranslationGroupSet._get to allow it to be defined as a collection.  Provided __getitem_ as getByName to match other root collections.  Renamed parameter to 'key' to match the model.

Implemented SourcePackage.deletePackaging instead of allowing setPackaging to take None, because lazr.restful doesn't support non-default None, and for symmetry with ProductSeries.setPackaging (which cannot work this way, since there may be multiple Packagings).

== Tests ==
bin/test -t xx-change-assignee.txt -t TestWebService

== Demo and Q/A ==


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/tests/test_productseries.py
  lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt
  lib/lp/translations/interfaces/translationpolicy.py
  lib/lp/translations/model/translationgroup.py
  lib/lp/registry/interfaces/sourcepackage.py
  lib/lp/registry/tests/test_sourcepackage.py
  lib/lp/translations/interfaces/translationgroup.py
  lib/lp/testing/__init__.py
  lib/lp/app/interfaces/launchpad.py
  lib/lp/translations/interfaces/webservice.py
  lib/lp/registry/interfaces/productseries.py
  lib/lp/registry/model/sourcepackage.py
  lib/lp/translations/tests/test_translationgroup.py
  lib/lp/registry/tests/test_product.py

./lib/lp/translations/interfaces/translationgroup.py
      45: 'TranslationPermission' imported but unused

^^^ This is imported so that other clients can import it from here.

./lib/lp/testing/__init__.py
     135: 'anonymous_logged_in' imported but unused
     135: 'with_anonymous_login' imported but unused
     154: 'launchpadlib_for' imported but unused
     154: 'launchpadlib_credentials_for' imported but unused
     135: 'with_person_logged_in' imported but unused
     135: 'person_logged_in' imported but unused
     154: 'oauth_access_token_for' imported but unused
     135: 'login_celebrity' imported but unused
     135: 'with_celebrity_logged_in' imported but unused
     153: 'test_tales' imported but unused
     135: 'celebrity_logged_in' imported but unused
     135: 'run_with_login' imported but unused
     135: 'is_logged_in' imported but unused
     135: 'login_team' imported but unused
     135: 'login_person' imported but unused
     135: 'login_as' imported but unused

^^^ These are imported so that other clients can import them from here.


./lib/lp/translations/interfaces/webservice.py
      26: 'IPOFile' imported but unused
      28: 'ITranslationGroupSet' imported but unused
      28: 'ITranslationGroup' imported but unused
      23: 'IHasTranslationImports' imported but unused
      32: 'ITranslationImportQueue' imported but unused
      32: 'ITranslationImportQueueEntry' imported but unused
      27: 'IPOTemplate' imported but unused

^^^ These are imported so that lazr.restful can disover them here.
-- 
https://code.launchpad.net/~abentley/launchpad/export-translation-api/+merge/52257
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/export-translation-api into lp:launchpad.
=== modified file 'lib/lp/app/interfaces/launchpad.py'
--- lib/lp/app/interfaces/launchpad.py	2010-08-25 00:01:57 +0000
+++ lib/lp/app/interfaces/launchpad.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces for the Launchpad application.
@@ -13,6 +13,7 @@
     'IServiceUsage',
     ]
 
+from lazr.restful.declarations import exported
 from zope.interface import Interface
 from zope.schema import (
     Bool,
@@ -45,11 +46,11 @@
         description=_("Where does this pillar host code?"),
         default=ServiceUsage.UNKNOWN,
         vocabulary=ServiceUsage)
-    translations_usage = Choice(
+    translations_usage = exported(Choice(
         title=_('Type of service for translations application'),
         description=_("Where does this pillar do translations?"),
         default=ServiceUsage.UNKNOWN,
-        vocabulary=ServiceUsage)
+        vocabulary=ServiceUsage))
     bug_tracking_usage = Choice(
         title=_('Type of service for tracking bugs'),
         description=_("Where does this pillar track bugs?"),

=== modified file 'lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt'
--- lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt	2010-10-09 16:36:22 +0000
+++ lib/lp/bugs/stories/bugtask-management/xx-change-assignee.txt	2011-03-04 21:43:00 +0000
@@ -1,4 +1,5 @@
-= Changing bug assignment =
+Changing bug assignment
+=======================
 
 A bug is unassigned by choosing the "Assigned to" -> "Nobody" option.
 
@@ -17,11 +18,13 @@
 
     >>> admin_browser.getControl("Save Changes", index=0).click()
 
-    >>> admin_browser.getControl(name="firefox.assignee.option", index=0).value
+    >>> admin_browser.getControl(
+    ...     name="firefox.assignee.option", index=0).value
     ['firefox.assignee.assign_to_nobody']
 
 
-== Bug assignment to non-contributors ==
+Bug assignment to non-contributors
+==================================
 
 When attempting to assign a bug to a user who isn't an established bug
 contributor (they have no bugs currently assigned to them) the user is
@@ -88,7 +91,8 @@
     None
 
 
-== Bug task assignment by regular users ==
+Bug task assignment by regular users
+====================================
 
 Regular users can only set themselves and their teams as assignees if
 there is a bug supervisor established for a project.
@@ -138,7 +142,7 @@
     >>> user_browser.getControl("Save Changes", index=0).click()
     >>> print_errors(user_browser.contents)
 
-But if he treis to set other persons or teams, he gets an error message.
+But if he tries to set other persons or teams, he gets an error message.
 
     >>> user_browser.open("http://bugs.launchpad.dev/jokosher/+bug/11";)
     >>> assignee_control = user_browser.getControl(

=== modified file 'lib/lp/registry/interfaces/productseries.py'
--- lib/lp/registry/interfaces/productseries.py	2011-02-24 15:30:54 +0000
+++ lib/lp/registry/interfaces/productseries.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -131,9 +131,6 @@
     ISpecificationGoal, IHasMilestones, IHasOfficialBugTags,
     IHasTranslationImports, IHasTranslationTemplates, IServiceUsage):
     """Public IProductSeries properties."""
-    # XXX Mark Shuttleworth 2004-10-14: Would like to get rid of id in
-    # interfaces, as soon as SQLobject allows using the object directly
-    # instead of using object.id.
     id = Int(title=_('ID'))
 
     product = exported(
@@ -237,12 +234,12 @@
             description=_("The Bazaar branch for this series.  Leave blank "
                           "if this series is not maintained in Bazaar.")))
 
-    translations_autoimport_mode = Choice(
+    translations_autoimport_mode = exported(Choice(
         title=_('Import settings'),
         vocabulary=TranslationsBranchImportMode,
         required=True,
         description=_("Specify which files will be imported from the "
-                      "source code branch."))
+                      "source code branch.")))
 
     potemplate_count = Int(
         title=_("The total number of POTemplates in this series."),

=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
--- lib/lp/registry/interfaces/sourcepackage.py	2010-11-15 16:25:05 +0000
+++ lib/lp/registry/interfaces/sourcepackage.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -54,6 +54,7 @@
     IHasCodeImports,
     IHasMergeProposals,
     )
+from lp.registry.interfaces.productseries import IProductSeries
 from lp.soyuz.interfaces.component import IComponent
 from lp.translations.interfaces.hastranslationtemplates import (
     IHasTranslationTemplates,
@@ -195,12 +196,19 @@
         sourcepackagename compare not equal.
         """
 
+    @operation_parameters(productseries=Reference(schema=IProductSeries))
+    @call_with(owner=REQUEST_USER)
+    @export_write_operation()
     def setPackaging(productseries, owner):
         """Update the existing packaging record, or create a new packaging
         record, that links the source package to the given productseries,
         and record that it was done by the owner.
         """
 
+    @export_write_operation()
+    def deletePackaging():
+        """Delete the packaging for this sourcepackage."""
+
     def getSuiteSourcePackage(pocket):
         """Return the `ISuiteSourcePackage` for this package in 'pocket'.
 

=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py	2011-01-27 22:17:07 +0000
+++ lib/lp/registry/model/sourcepackage.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212
@@ -13,12 +13,10 @@
 
 from operator import attrgetter
 
-from sqlobject.sqlbuilder import SQLConstant
 from storm.locals import (
     And,
     Desc,
     Select,
-    SQL,
     Store,
     )
 from zope.component import getUtility
@@ -529,23 +527,30 @@
             'BugTask.distroseries = %s AND BugTask.sourcepackagename = %s' %
                 sqlvalues(self.distroseries, self.sourcepackagename))
 
-    def setPackaging(self, productseries, user):
+    def setPackaging(self, productseries, owner):
+        """See `ISourcePackage`."""
         target = self.direct_packaging
         if target is not None:
             # we should update the current packaging
             target.productseries = productseries
-            target.owner = user
+            target.owner = owner
             target.datecreated = UTC_NOW
         else:
             # ok, we need to create a new one
             Packaging(
                 distroseries=self.distroseries,
                 sourcepackagename=self.sourcepackagename,
-                productseries=productseries, owner=user,
+                productseries=productseries, owner=owner,
                 packaging=PackagingType.PRIME)
         # and make sure this change is immediately available
         flush_database_updates()
 
+    def deletePackaging(self):
+        """See `ISourcePackage`."""
+        if self.direct_packaging is None:
+            return
+        self.direct_packaging.destroySelf()
+
     def __hash__(self):
         """See `ISourcePackage`."""
         return hash(self.distroseries.id) ^ hash(self.sourcepackagename.id)

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2011-01-06 15:48:25 +0000
+++ lib/lp/registry/tests/test_product.py	2011-03-04 21:43:00 +0000
@@ -21,6 +21,7 @@
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     )
+from lp.app.enums import ServiceUsage
 from lp.registry.interfaces.product import (
     IProduct,
     License,
@@ -36,7 +37,9 @@
     login,
     login_person,
     TestCaseWithFactory,
+    WebServiceTestCase,
     )
+from lp.translations.enums import TranslationPermission
 
 
 class TestProduct(TestCaseWithFactory):
@@ -354,5 +357,34 @@
             self.product.bug_supervisor.name))
 
 
+class TestWebService(WebServiceTestCase):
+
+    def test_translations_usage(self):
+        """The translations_usage field should be writable."""
+        product = self.factory.makeProduct()
+        transaction.commit()
+        ws_product = self.wsObject(product, product.owner)
+        ws_product.translations_usage = ServiceUsage.EXTERNAL.title
+        ws_product.lp_save()
+
+    def test_translationpermission(self):
+        """The translationpermission field should be writable."""
+        product = self.factory.makeProduct()
+        transaction.commit()
+        ws_product = self.wsObject(product, product.owner)
+        ws_product.translationpermission = TranslationPermission.OPEN.title
+        ws_product.lp_save()
+
+    def test_translationgroup(self):
+        """The translationgroup field should be writable."""
+        product = self.factory.makeProduct()
+        group = self.factory.makeTranslationGroup()
+        transaction.commit()
+        ws_product = self.wsObject(product, product.owner)
+        ws_group = self.wsObject(group)
+        ws_product.translationgroup = ws_group
+        ws_product.lp_save()
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/registry/tests/test_productseries.py'
--- lib/lp/registry/tests/test_productseries.py	2011-01-12 18:05:13 +0000
+++ lib/lp/registry/tests/test_productseries.py	2011-03-04 21:43:00 +0000
@@ -1,10 +1,11 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for ProductSeries and ProductSeriesSet."""
 
 __metaclass__ = type
 
+import transaction
 from zope.component import getUtility
 
 from canonical.launchpad.ftests import login
@@ -19,7 +20,10 @@
     IProductSeries,
     IProductSeriesSet,
     )
-from lp.testing import TestCaseWithFactory
+from lp.testing import (
+    TestCaseWithFactory,
+    WebServiceTestCase,
+    )
 from lp.testing.matchers import DoesNotSnapshot
 from lp.translations.interfaces.translations import (
     TranslationsBranchImportMode,
@@ -318,3 +322,15 @@
         self.assertThat(
             productseries,
             DoesNotSnapshot(skipped, IProductSeries))
+
+
+class TestWebService(WebServiceTestCase):
+
+    def test_translations_autoimport_mode(self):
+        """Autoimport mode can be set over Web Service."""
+        series = self.factory.makeProductSeries()
+        transaction.commit()
+        ws_series = self.wsObject(series, series.owner)
+        mode = TranslationsBranchImportMode.IMPORT_TRANSLATIONS
+        ws_series.translations_autoimport_mode = mode.title
+        ws_series.lp_save()

=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
--- lib/lp/registry/tests/test_sourcepackage.py	2010-10-26 15:47:24 +0000
+++ lib/lp/registry/tests/test_sourcepackage.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for ISourcePackage implementations."""
@@ -7,6 +7,7 @@
 
 import unittest
 
+import transaction
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
@@ -31,6 +32,7 @@
 from lp.testing import (
     person_logged_in,
     TestCaseWithFactory,
+    WebServiceTestCase,
     )
 from lp.testing.views import create_initialized_view
 
@@ -255,6 +257,39 @@
         self.assertEqual(''.join(expected_summary), sp.summary)
 
 
+class TestSourcePackageWebService(WebServiceTestCase):
+
+    def test_setPackaging(self):
+        """setPackaging is accessible and works."""
+        sourcepackage = self.factory.makeSourcePackage()
+        self.assertIs(None, sourcepackage.direct_packaging)
+        productseries = self.factory.makeProductSeries()
+        transaction.commit()
+        ws_sourcepackage = self.wsObject(sourcepackage)
+        ws_productseries = self.wsObject(productseries)
+        ws_sourcepackage.setPackaging(productseries=ws_productseries)
+        transaction.commit()
+        self.assertEqual(
+            productseries, sourcepackage.direct_packaging.productseries)
+
+    def test_deletePackaging(self):
+        """Deleting a packaging should work."""
+        packaging = self.factory.makePackagingLink()
+        sourcepackage = packaging.sourcepackage
+        transaction.commit()
+        self.wsObject(sourcepackage).deletePackaging()
+        transaction.commit()
+        self.assertIs(None, sourcepackage.direct_packaging)
+
+    def test_deletePackaging_with_no_packaging(self):
+        """Deleting when there's no packaging should be a no-op."""
+        sourcepackage = self.factory.makeSourcePackage()
+        transaction.commit()
+        self.wsObject(sourcepackage).deletePackaging()
+        transaction.commit()
+        self.assertIs(None, sourcepackage.direct_packaging)
+
+
 class TestSourcePackageSecurity(TestCaseWithFactory):
     """Tests for source package branch linking security."""
 

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-02-25 02:08:52 +0000
+++ lib/lp/testing/__init__.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009, 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=W0401,C0301,F0401
@@ -785,6 +785,32 @@
         return client, obj_url
 
 
+class WebServiceTestCase(TestCaseWithFactory):
+    """Test case optimized for testing the web service using launchpadlib."""
+
+    #avoid circular imports
+    from canonical.testing.layers import AppServerLayer
+
+    layer = AppServerLayer
+
+    def setUp(self):
+        super(WebServiceTestCase, self).setUp()
+        self.service = self.factory.makeLaunchpadService()
+
+    def wsObject(self, obj, user=None):
+        """Return the launchpadlib version of the supplied object.
+
+        :param obj: The object to find the launchpadlib equivalent of.
+        :param user: The user to use for accessing the object over
+            lauchpadlib.  Defaults to an arbitrary logged-in user.
+        """
+        if user is not None:
+            service = self.factory.makeLaunchpadService(user)
+        else:
+            service = self.service
+        return ws_object(service, obj)
+
+
 def quote_jquery_expression(expression):
     """jquery requires meta chars used in literals escaped with \\"""
     return re.sub(
@@ -1155,11 +1181,8 @@
     :param obj: The object to convert.
     :return: A launchpadlib Entry object.
     """
-    api_request = WebServiceTestRequest()
-    obj_url = canonical_url(obj, request=api_request)
-    return launchpad.load(
-        obj_url.replace('http://api.launchpad.dev/',
-        str(launchpad._root_uri)))
+    api_request = WebServiceTestRequest(SERVER_URL=str(launchpad._root_uri))
+    return launchpad.load(canonical_url(obj, request=api_request))
 
 
 @contextmanager

=== modified file 'lib/lp/translations/interfaces/translationgroup.py'
--- lib/lp/translations/interfaces/translationgroup.py	2011-02-23 20:26:53 +0000
+++ lib/lp/translations/interfaces/translationgroup.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213
@@ -10,8 +10,19 @@
 __all__ = [
     'ITranslationGroup',
     'ITranslationGroupSet',
+    'TranslationPermission',
     ]
 
+from lazr.restful.declarations import (
+    collection_default_content,
+    exported,
+    export_as_webservice_entry,
+    export_as_webservice_collection,
+    export_read_operation,
+    export_operation_as,
+    operation_parameters,
+    operation_returns_entry,
+    )
 from zope.interface import (
     Attribute,
     Interface,
@@ -37,17 +48,20 @@
 class ITranslationGroup(IHasOwner):
     """A TranslationGroup."""
 
+    export_as_webservice_entry(
+        singular_name='translation_group', plural_name='translation_groups')
+
     id = Int(
             title=_('Translation Group ID'), required=True, readonly=True,
             )
-    name = TextLine(
+    name = exported(TextLine(
             title=_('Name'), required=True,
             description=_("""Keep this name very short, unique, and
             descriptive, because it will be used in URLs. Examples:
             gnome-translation-project, ubuntu-translators."""),
             constraint=name_validator,
-            )
-    title = Title(
+            ))
+    title = exported(Title(
             title=_('Title'), required=True,
             description=_("""Title of this Translation Group.
             This title is displayed at the top of the Translation Group
@@ -55,7 +69,7 @@
             add "translation group" to this title, or it will be shown
             double.
             """),
-            )
+            ))
     summary = Summary(
             title=_('Summary'), required=True,
             description=_("""A single-paragraph description of the
@@ -148,11 +162,22 @@
 class ITranslationGroupSet(Interface):
     """A container for translation groups."""
 
+    export_as_webservice_collection(ITranslationGroup)
+
     title = Attribute('Title')
 
-    def __getitem__(key):
+    @operation_parameters(
+        name=TextLine(title=_("Name of the translation group"),))
+    @operation_returns_entry(ITranslationGroup)
+    @export_operation_as('getByName')
+    @export_read_operation()
+    def __getitem__(name):
         """Get a translation group by name."""
 
+    @collection_default_content()
+    def _get():
+        """Return a collection of all entries."""
+
     def __iter__():
         """Iterate through the translation groups in this set."""
 

=== modified file 'lib/lp/translations/interfaces/translationpolicy.py'
--- lib/lp/translations/interfaces/translationpolicy.py	2010-11-10 05:04:16 +0000
+++ lib/lp/translations/interfaces/translationpolicy.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Translation access and sharing policy."""
@@ -8,11 +8,18 @@
     'ITranslationPolicy',
     ]
 
+from lazr.restful.declarations import (
+    exported,
+    )
+from lazr.restful.fields import (
+    ReferenceChoice,
+    )
 from zope.interface import Interface
 from zope.schema import Choice
 
 from canonical.launchpad import _
 from lp.translations.enums import TranslationPermission
+from lp.translations.interfaces.translationgroup import ITranslationGroup
 
 
 class ITranslationPolicy(Interface):
@@ -32,20 +39,21 @@
     user: translation team and translation policy.
     """
 
-    translationgroup = Choice(
+    translationgroup = exported(ReferenceChoice(
         title = _("Translation group"),
         description = _("The translation group that helps review "
             " translations for this project or distribution. The group's "
             " role depends on the permissions policy selected below."),
         required=False,
-        vocabulary='TranslationGroup')
+        vocabulary='TranslationGroup',
+        schema=ITranslationGroup))
 
-    translationpermission = Choice(
+    translationpermission = exported(Choice(
         title=_("Translation permissions policy"),
         description=_("The policy this project or distribution uses to "
             " balance openness and control for their translations."),
         required=True,
-        vocabulary=TranslationPermission)
+        vocabulary=TranslationPermission))
 
     def getTranslationGroups():
         """List all applicable translation groups.

=== modified file 'lib/lp/translations/interfaces/webservice.py'
--- lib/lp/translations/interfaces/webservice.py	2010-11-09 16:25:22 +0000
+++ lib/lp/translations/interfaces/webservice.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009, 2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=W0611
@@ -15,6 +15,7 @@
     'IHasTranslationImports',
     'IPOFile',
     'IPOTemplate',
+    'ITranslationGroup',
     'ITranslationImportQueue',
     'ITranslationImportQueueEntry',
     ]
@@ -24,6 +25,10 @@
     )
 from lp.translations.interfaces.pofile import IPOFile
 from lp.translations.interfaces.potemplate import IPOTemplate
+from lp.translations.interfaces.translationgroup import (
+    ITranslationGroup,
+    ITranslationGroupSet,
+)
 from lp.translations.interfaces.translationimportqueue import (
     ITranslationImportQueue,
     ITranslationImportQueueEntry,

=== modified file 'lib/lp/translations/model/translationgroup.py'
--- lib/lp/translations/model/translationgroup.py	2010-12-02 16:13:51 +0000
+++ lib/lp/translations/model/translationgroup.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212
@@ -35,7 +35,9 @@
     LibraryFileAlias,
     LibraryFileContent,
     )
-from canonical.launchpad.interfaces.lpstorm import ISlaveStore
+from canonical.launchpad.interfaces.lpstorm import (
+    ISlaveStore,
+    )
 from lp.app.errors import NotFoundError
 from lp.registry.interfaces.person import validate_public_person
 from lp.registry.model.person import Person
@@ -178,7 +180,7 @@
             Language.id == Translator.languageID,
             Person.id == Translator.translatorID)
         translator_data = translator_data.order_by(Language.englishname)
-        mapper = lambda row:row[slice(0,3)]
+        mapper = lambda row: row[slice(0, 3)]
         return DecoratedResultSet(translator_data, mapper)
 
     def fetchProjectsForDisplay(self):
@@ -286,6 +288,9 @@
         except SQLObjectNotFound:
             raise NotFoundError(name)
 
+    def _get(self):
+        return self
+
     def new(self, name, title, summary, translation_guide_url, owner):
         """See ITranslationGroupSet."""
         return TranslationGroup(

=== modified file 'lib/lp/translations/tests/test_translationgroup.py'
--- lib/lp/translations/tests/test_translationgroup.py	2010-10-04 19:50:45 +0000
+++ lib/lp/translations/tests/test_translationgroup.py	2011-03-04 21:43:00 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for `TranslationGroup` and related classes."""
@@ -7,6 +7,8 @@
 
 from unittest import TestLoader
 
+from lazr.restfulclient.errors import Unauthorized
+import transaction
 from zope.component import getUtility
 
 from canonical.testing.layers import ZopelessDatabaseLayer
@@ -14,7 +16,10 @@
     ITeamMembershipSet,
     TeamMembershipStatus,
     )
-from lp.testing import TestCaseWithFactory
+from lp.testing import (
+    TestCaseWithFactory,
+    WebServiceTestCase,
+    )
 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
 
 
@@ -63,5 +68,26 @@
             list(getUtility(ITranslationGroupSet).getByPerson(person)))
 
 
+class TestWebService(WebServiceTestCase):
+
+    def test_getByName(self):
+        """getByName returns the TranslationGroup for the specified name."""
+        group = self.factory.makeTranslationGroup()
+        transaction.commit()
+        ws_group = self.service.translation_groups.getByName(name=group.name)
+        self.assertEqual(group.name, ws_group.name)
+
+    def test_attrs(self):
+        """TranslationGroup provides the expected attributes."""
+        group = self.factory.makeTranslationGroup()
+        transaction.commit()
+        ws_group = self.wsObject(group)
+        self.assertEqual(group.name, ws_group.name)
+        self.assertEqual(group.title, ws_group.title)
+        ws_group.name = 'foo'
+        e = self.assertRaises(Unauthorized, ws_group.lp_save)
+        self.assertIn("'name', 'launchpad.Edit'", str(e))
+
+
 def test_suite():
     return TestLoader().loadTestsFromName(__name__)


Follow ups