launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00130
[Merge] lp:~jtv/launchpad/translationtemplatescollection-test into lp:launchpad/devel
Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/translationtemplatescollection-test into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers): code
= TranslationTemplatesCollection test =
This branch provides intermediate testing for TranslationTemplatesCollection, layered between the existing tests for Collection and for the one class implemented in terms of collections, HasTranslationTemplatesMixin. A silly typo in the TranslationTemplatesCollection revealed a gap in test coverage, which was a good excuse for both fixing the bug and testing the class separately. I should have done it from the start.
This also introduces another piece of convenience for Collection: a concrete collection class based on Collection selects, by default, just the class that it is a collection of. So calling TranslationTemplatesCollection.select() without arguments is now allowed, and returns a sequence of POTemplate.
There's also some drive-by lint cleanup. However I left some pre-existing ones in place because what "make lint" is complaining about looks to me an awful lot like our preferred syntax:
list_comprehension = [
frobnicate(item)
for item in items
if effable(item)
]
Jeroen
--
https://code.launchpad.net/~jtv/launchpad/translationtemplatescollection-test/+merge/30114
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/translationtemplatescollection-test into lp:launchpad/devel.
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2010-06-22 19:42:51 +0000
+++ lib/lp/registry/model/distroseries.py 2010-07-16 14:43:43 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=E0611,W0212
@@ -71,7 +71,8 @@
from canonical.launchpad.database.librarian import LibraryFileAlias
from lp.translations.model.potemplate import (
HasTranslationTemplatesMixin,
- POTemplate)
+ POTemplate,
+ TranslationTemplatesCollection)
from lp.soyuz.model.publishing import (
BinaryPackagePublishingHistory, SourcePackagePublishingHistory)
from lp.soyuz.model.queue import PackageUpload, PackageUploadQueue
@@ -1871,40 +1872,9 @@
def main_archive(self):
return self.distribution.main_archive
- def getTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.selectBy(distroseries=self,
- orderBy=['-priority', 'name'])
- return result
-
- def getCurrentTranslationTemplates(self, just_ids=False):
- """See `IHasTranslationTemplates`."""
- store = Store.of(self)
- if just_ids:
- looking_for = POTemplate.id
- else:
- looking_for = POTemplate
-
- # Select all current templates for this series, if the Product
- # actually uses Launchpad Translations. Otherwise, return an
- # empty result.
- result = store.find(looking_for, And(
- self.distribution.official_rosetta == True,
- POTemplate.iscurrent == True,
- POTemplate.distroseries == self))
- return result.order_by(['-POTemplate.priority', 'POTemplate.name'])
-
- def getObsoleteTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.select('''
- distroseries = %s AND
- distroseries = DistroSeries.id AND
- DistroSeries.distribution = Distribution.id AND
- (iscurrent IS FALSE OR Distribution.official_rosetta IS FALSE)
- ''' % sqlvalues(self),
- clauseTables = ['DistroSeries', 'Distribution'],
- orderBy=['-priority', 'name'])
- return shortlist(result, 300)
+ def getTemplatesCollection(self):
+ """See `IHasTranslationTemplates`."""
+ return TranslationTemplatesCollection().restrictDistroSeries(self)
def getSuite(self, pocket):
"""See `IDistroSeries`."""
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2010-06-09 08:26:26 +0000
+++ lib/lp/registry/model/productseries.py 2010-07-16 14:43:43 +0000
@@ -40,7 +40,8 @@
from lp.translations.model.pofile import POFile
from lp.translations.model.potemplate import (
HasTranslationTemplatesMixin,
- POTemplate)
+ POTemplate,
+ TranslationTemplatesCollection)
from lp.registry.model.productrelease import ProductRelease
from lp.translations.model.productserieslanguage import (
ProductSeriesLanguage)
@@ -50,7 +51,6 @@
HasTranslationImportsMixin)
from lp.registry.model.structuralsubscription import (
StructuralSubscriptionTargetMixin)
-from canonical.launchpad.helpers import shortlist
from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from lp.registry.interfaces.packaging import PackagingType
from lp.blueprints.interfaces.specification import (
@@ -428,46 +428,15 @@
name=name, dateexpected=dateexpected, summary=summary,
product=self.product, productseries=self, code_name=code_name)
- def getTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.selectBy(
- productseries=self, orderBy=['-priority', 'name'])
- return result
-
- def getCurrentTranslationTemplates(self, just_ids=False):
- """See `IHasTranslationTemplates`."""
- store = Store.of(self)
- if just_ids:
- looking_for = POTemplate.id
- else:
- looking_for = POTemplate
-
- # Select all current templates for this series, if the Product
- # actually uses Launchpad Translations. Otherwise, return an
- # empty result.
- result = store.find(looking_for, And(
- self.product.official_rosetta == True,
- POTemplate.iscurrent == True,
- POTemplate.productseries == self))
- return result.order_by(['-POTemplate.priority', 'POTemplate.name'])
+ def getTemplatesCollection(self):
+ """See `IHasTranslationTemplates`."""
+ return TranslationTemplatesCollection().restrictProductSeries(self)
@property
def potemplate_count(self):
"""See `IProductSeries`."""
return self.getCurrentTranslationTemplates().count()
- def getObsoleteTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.select('''
- productseries = %s AND
- productseries = ProductSeries.id AND
- ProductSeries.product = Product.id AND
- (iscurrent IS FALSE OR Product.official_rosetta IS FALSE)
- ''' % sqlvalues(self),
- orderBy=['-priority', 'name'],
- clauseTables = ['ProductSeries', 'Product'])
- return shortlist(result, 300)
-
@property
def productserieslanguages(self):
"""See `IProductSeries`."""
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2010-07-01 19:21:45 +0000
+++ lib/lp/registry/model/sourcepackage.py 2010-07-16 14:43:43 +0000
@@ -39,7 +39,8 @@
from lp.registry.model.packaging import Packaging
from lp.translations.model.potemplate import (
HasTranslationTemplatesMixin,
- POTemplate)
+ POTemplate,
+ TranslationTemplatesCollection)
from canonical.launchpad.interfaces.lpstorm import IStore
from lp.soyuz.model.publishing import (
SourcePackagePublishingHistory)
@@ -619,39 +620,11 @@
else:
return distribution.main_archive
- def getTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.selectBy(
- distroseries=self.distroseries,
- sourcepackagename=self.sourcepackagename)
- return result.orderBy(['-priority', 'name'])
-
- def getCurrentTranslationTemplates(self, just_ids=False):
- """See `IHasTranslationTemplates`."""
- store = Store.of(self.sourcepackagename)
- if just_ids:
- looking_for = POTemplate.id
- else:
- looking_for = POTemplate
-
- result = store.find(looking_for, And(
- POTemplate.iscurrent == True,
- POTemplate.distroseries == self.distroseries,
- POTemplate.sourcepackagename == self.sourcepackagename,
- self.distroseries.distribution.official_rosetta == True))
- return result.order_by(['-POTemplate.priority', 'POTemplate.name'])
-
- def getObsoleteTranslationTemplates(self):
- """See `IHasTranslationTemplates`."""
- result = POTemplate.select('''
- distroseries = %s AND
- sourcepackagename = %s AND
- distroseries = DistroSeries.id AND
- DistroSeries.distribution = Distribution.id AND
- (iscurrent IS FALSE OR Distribution.official_rosetta IS FALSE)
- ''' % sqlvalues(self.distroseries, self.sourcepackagename),
- clauseTables = ['DistroSeries', 'Distribution'])
- return shortlist(result.orderBy(['-priority', 'name']), 300)
+ def getTemplatesCollection(self):
+ """See `IHasTranslationTemplates`."""
+ collection = TranslationTemplatesCollection()
+ collection = collection.restrictDistroSeries(self.distroseries)
+ return collection.restrictSourcePackageName(self.sourcepackagename)
def getBranch(self, pocket):
"""See `ISourcePackage`."""
=== modified file 'lib/lp/registry/templates/distroseries-packaging.pt'
--- lib/lp/registry/templates/distroseries-packaging.pt 2010-06-30 18:31:18 +0000
+++ lib/lp/registry/templates/distroseries-packaging.pt 2010-07-16 14:43:43 +0000
@@ -57,7 +57,7 @@
<tr tal:define="series packaging/productseries;
product series/product;
package packaging/sourcepackage;
- translations package/getTranslationTemplates;">
+ translations package/has_translation_templates;">
<td>
<a class="sprite package-source"
tal:attributes="href package/fmt:url"
=== modified file 'lib/lp/registry/templates/sourcepackage-upstream-connections.pt'
--- lib/lp/registry/templates/sourcepackage-upstream-connections.pt 2010-03-02 20:09:08 +0000
+++ lib/lp/registry/templates/sourcepackage-upstream-connections.pt 2010-07-16 14:43:43 +0000
@@ -50,7 +50,7 @@
<tal:yes-no replace="structure series/branch/image:boolean"/>
</dd>
<dd title="Series translations auto import"
- tal:condition="context/getTranslationTemplates"
+ tal:condition="context/has_translation_templates"
tal:define="bool not:series/translations_autoimport_mode/enumvalue:NO_IMPORT">
Translations:
<tal:yes-no replace="structure bool/image:boolean"/>
=== added file 'lib/lp/services/database/collection.py'
--- lib/lp/services/database/collection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/database/collection.py 2010-07-16 14:43:43 +0000
@@ -0,0 +1,139 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A generic collection of database objects."""
+
+__metaclass__ = type
+__all__ = [
+ 'Collection',
+ ]
+
+from zope.component import getUtility
+
+from storm.expr import Join, LeftJoin
+
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR, IStoreSelector, MAIN_STORE)
+
+
+class Collection(object):
+ """An arbitrary collection of database objects.
+
+ Works as a Storm wrapper: create a collection based on another
+ collection, adding joins and select conditions to taste.
+
+ As in any Storm query, you can select any mix of classes and
+ individual columns or other Storm expressions.
+ """
+
+ # Default table for this collection that will always be included.
+ # Derived collection classes can use this to say what type they are
+ # a collection of.
+ starting_table = None
+
+ def __init__(self, *args, **kwargs):
+ """Construct a collection, possibly based on another one.
+
+ :param base: Optional collection that this collection is based
+ on. The new collection will inherit its configuration.
+ :param conditions: Optional Storm select conditions, e.g.
+ `MyClass.attribute > 2`.
+ :param classes: A class, or tuple or list of classes, that
+ should go into the "FROM" clause of the new collection.
+ This need not include classes that are already in the
+ base collection, or that are included as outer joins.
+ :param store: Optional: Storm `Store` to use.
+ """
+ starting_tables = []
+
+ if len(args) >= 1 and isinstance(args[0], Collection):
+ # There's a base collection.
+ base = args[0]
+ conditions = args[1:]
+ else:
+ # We're starting a fresh collection.
+ base = None
+ conditions = args
+ if self.starting_table is not None:
+ starting_tables = [self.starting_table]
+
+ self.base = base
+
+ if base is None:
+ base_conditions = (True, )
+ base_tables = []
+ else:
+ self.store = base.store
+ base_conditions = base.conditions
+ base_tables = list(base.tables)
+
+ self.store = kwargs.get('store')
+ if self.store is None:
+ self.store = getUtility(IStoreSelector).get(
+ MAIN_STORE, DEFAULT_FLAVOR)
+
+ self.tables = (
+ starting_tables + base_tables +
+ self._parseTablesArg(kwargs.get('tables', [])))
+
+ self.conditions = base_conditions + conditions
+
+ def refine(self, *args, **kwargs):
+ """Return a copy of self with further restrictions, tables etc."""
+ cls = self.__class__
+ return cls(self, *args, **kwargs)
+
+ def _parseTablesArg(self, tables):
+ """Turn tables argument into a list.
+
+ :param tables: A class, or tuple of classes, or list of classes.
+ :param return: All classes that were passed in, as a list.
+ """
+ if isinstance(tables, tuple):
+ return list(tables)
+ elif isinstance(tables, list):
+ return tables
+ else:
+ return [tables]
+
+ def use(self, store):
+ """Return a copy of this collection that uses the given store."""
+ return self.refine(store=store)
+
+ def joinInner(self, cls, *conditions):
+ """Convenience method: inner-join `cls` into the query.
+
+ This is equivalent to creating a `Collection` based on this one
+ but with `cls` and `conditions` added.
+ """
+ return self.refine(tables=[Join(cls, *conditions)])
+
+ def joinOuter(self, cls, *conditions):
+ """Outer-join `cls` into the query."""
+ return self.refine(tables=[LeftJoin(cls, *conditions)])
+
+ def select(self, *values):
+ """Return a result set containing the requested `values`.
+
+ If no values are requested, this selects the type of object that
+ the Collection is a collection of.
+ """
+ if len(self.tables) == 0:
+ source = self.store
+ else:
+ source = self.store.using(*self.tables)
+
+ if len(values) > 1:
+ # Selecting a tuple of values. Pass it to Storm unchanged.
+ pass
+ elif len(values) == 1:
+ # One value requested. Unpack for convenience.
+ values = values[0]
+ else:
+ # Select the starting table by default.
+ assert self.starting_table is not None, (
+ "Collection %s does not define a starting table." %
+ self.__class__.__name__)
+ values = self.starting_table
+
+ return source.find(values, *self.conditions)
=== added file 'lib/lp/services/database/tests/test_collection.py'
--- lib/lp/services/database/tests/test_collection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/database/tests/test_collection.py 2010-07-16 14:43:43 +0000
@@ -0,0 +1,186 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `Collection`."""
+
+__metaclass__ = type
+
+from storm.locals import Int, Storm
+from zope.component import getUtility
+
+from lp.services.database.collection import Collection
+from lp.testing import TestCaseWithFactory
+from lp.testing.fakemethod import FakeMethod
+from canonical.testing import ZopelessDatabaseLayer
+from canonical.launchpad.webapp.interfaces import (
+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
+
+
+def get_store():
+ return getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+
+
+class FakeStore:
+ find = FakeMethod(result=[])
+
+ def using(self, *args):
+ return self
+
+
+def make_table(range_start, range_end, table_name=None):
+ """Create a temporary table and a storm class for it."""
+ assert range_start < range_end, "Invalid range."
+ if table_name is None:
+ table_name = "TestTable"
+ get_store().execute("""
+ CREATE TEMP TABLE %s AS
+ SELECT generate_series AS id
+ FROM generate_series(%d, %d)
+ """ % (table_name, range_start, range_end - 1))
+
+ class TestTable(Storm):
+ """A test class/table generated on the fly for testing purposes."""
+ __storm_table__ = table_name
+ id = Int(primary=True)
+
+ def __init__(self, id):
+ self.id = id
+
+ def __eq__(self, other):
+ return self.id == other.id
+
+ return TestTable
+
+
+def get_ids(testtable_objects):
+ """Helper to unpack ids from a sequence of TestTable objects."""
+ return [obj.id for obj in testtable_objects]
+
+
+class CollectionTest(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_make_table(self):
+ TestTable = make_table(1, 5)
+ result = get_store().find(TestTable).order_by(TestTable.id)
+ self.assertEqual(range(1, 5), get_ids(result))
+
+ def test_select_one(self):
+ TestTable = make_table(1, 5)
+ collection = Collection(TestTable.id == 1, tables=TestTable)
+ result = collection.select(TestTable)
+ self.assertEqual([1], get_ids(result))
+
+ def test_select_all(self):
+ TestTable = make_table(1, 3)
+ collection = Collection(tables=TestTable)
+ result = collection.select(TestTable)
+ self.assertContentEqual([1, 2], get_ids(result))
+
+ def test_select_condition(self):
+ TestTable = make_table(1, 5)
+ collection = Collection(TestTable.id > 2, tables=TestTable)
+ result = collection.select(TestTable)
+ self.assertContentEqual([3, 4], get_ids(result))
+
+ def test_select_conditions(self):
+ TestTable = make_table(1, 5)
+ collection = Collection(
+ TestTable.id > 2, TestTable.id < 4, tables=TestTable)
+ result = collection.select(TestTable)
+ self.assertContentEqual([3], get_ids(result))
+
+ def test_select_column(self):
+ TestTable = make_table(1, 3)
+ collection = Collection(tables=TestTable)
+ result = collection.select(TestTable.id)
+ self.assertContentEqual([1, 2], list(result))
+
+ def test_copy_collection(self):
+ TestTable = make_table(1, 3)
+ collection = Collection(tables=TestTable)
+ copied_collection = Collection(collection)
+ result = copied_collection.select(TestTable)
+ self.assertContentEqual([1, 2], get_ids(result))
+
+ def test_restrict_collection(self):
+ TestTable = make_table(1, 3)
+ collection = Collection(tables=TestTable)
+ copied_collection = Collection(collection, TestTable.id < 2)
+ result = copied_collection.select(TestTable)
+ self.assertContentEqual([1], get_ids(result))
+
+ def test_add_tables(self):
+ # The list of tables to select from carries across copies.
+ TestTable1 = make_table(1, 2, 'TestTable1')
+ TestTable2 = make_table(2, 3, 'TestTable2')
+ collection = Collection(tables=TestTable1)
+ collection = Collection(collection, tables=TestTable2)
+ result = collection.select(TestTable1.id, TestTable2.id)
+ self.assertEqual([(1, 2)], list(result))
+
+ def test_add_tables_and_conditions(self):
+ TestTable1 = make_table(1, 2, 'TestTable1')
+ TestTable2 = make_table(2, 3, 'TestTable2')
+ collection = Collection(TestTable1.id == 1, tables=TestTable1)
+ collection = Collection(
+ collection, TestTable2.id == 2, tables=TestTable2)
+ result = collection.select(TestTable1.id, TestTable2.id)
+ self.assertEqual([(1, 2)], list(result))
+
+ def test_select_join(self):
+ TestTable1 = make_table(1, 2, 'TestTable1')
+ TestTable2 = make_table(2, 3, 'TestTable2')
+ collection = Collection(tables=(TestTable1, TestTable2))
+ result = collection.select(TestTable1, TestTable2)
+ self.assertEqual(
+ [(TestTable1(id=1), TestTable2(id=2))], list(result))
+
+ def test_select_join_column(self):
+ TestTable1 = make_table(1, 2, 'TestTable1')
+ TestTable2 = make_table(2, 3, 'TestTable2')
+ collection = Collection(tables=(TestTable1, TestTable2))
+ result = collection.select(TestTable1.id, TestTable2.id)
+ self.assertEqual([(1, 2)], list(result))
+
+ def test_select_partial_join(self):
+ TestTable1 = make_table(1, 2, 'TestTable1')
+ TestTable2 = make_table(2, 3, 'TestTable2')
+ collection = Collection(
+ TestTable2.id == TestTable1.id + 1,
+ tables=(TestTable1, TestTable2))
+ result = collection.select(TestTable1.id)
+ self.assertEqual([1], list(result))
+
+ def test_joinInner(self):
+ TestTable1 = make_table(1, 3, 'TestTable1')
+ TestTable2 = make_table(2, 4, 'TestTable2')
+
+ # Add a join table to the collection.
+ collection = Collection(tables=TestTable1).joinInner(
+ TestTable2, TestTable2.id == TestTable1.id)
+ result = collection.select(TestTable1.id, TestTable2.id)
+ self.assertContentEqual([(2, 2)], list(result))
+
+ def test_joinOuter(self):
+ TestTable1 = make_table(1, 3, 'TestTable1')
+ TestTable2 = make_table(2, 4, 'TestTable2')
+
+ # Add an outer-join table to the collection.
+ collection = Collection(tables=TestTable1).joinOuter(
+ TestTable2, TestTable2.id == TestTable1.id)
+ result = collection.select(TestTable1.id, TestTable2.id)
+ self.assertContentEqual([(1, None), (2, 2)], list(result))
+
+ def test_select_store(self):
+ TestTable = make_table(1, 2)
+ collection = Collection(tables=TestTable)
+
+ store = FakeStore()
+ self.assertNotEqual(store, collection.store)
+
+ collection_with_store = collection.use(store)
+ self.assertEqual(store, collection_with_store.store)
+
+ self.assertEqual([], collection_with_store.select(TestTable))
=== added file 'lib/lp/services/doc/collection.txt'
--- lib/lp/services/doc/collection.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/doc/collection.txt 2010-07-16 14:43:43 +0000
@@ -0,0 +1,95 @@
+Collection
+==========
+
+The Collection class is a generic base for the Collection pattern: you
+can have a Collection of branches, a Collection of translation templates
+and so on. This is a more flexible and generic version of Sets and
+Subsets.
+
+The base Collection class is a very thin wrapper around Storm. You use
+it by deriving your own collection type from it. In this example, we
+look at collections of Kumquats.
+
+Kumquats are very simple things. All they have is a key to identify
+them.
+
+ >>> from storm.locals import Count, Int, Storm
+ >>> from canonical.launchpad.webapp.interfaces import (
+ ... IStoreSelector, MAIN_STORE, MASTER_FLAVOR)
+
+ >>> store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
+ >>> ok = store.execute("CREATE TEMP TABLE Kumquat(id integer UNIQUE)")
+ >>> class Kumquat(Storm):
+ ... __storm_table__ = 'Kumquat'
+ ... id = Int(primary=True)
+ ... def __init__(self, id):
+ ... self.id = id
+ ... def __repr__(self):
+ ... return "Kumquat-%d" % self.id
+
+ >>> obj = store.add(Kumquat(1))
+ >>> obj = store.add(Kumquat(2))
+
+A custom KumquatCollection class derives from Collection. The
+starting_table attribute tells Collection what it is a collection of.
+
+ >>> from lp.services.database.collection import Collection
+ >>> class KumquatCollection(Collection):
+ ... starting_table = Kumquat
+
+The collection starts out "containing" all kumquats. Nothing is queried
+yet until you invoke the "select" method, which returns a Storm result
+set.
+
+ >>> collection = KumquatCollection()
+ >>> print list(collection.select().order_by(Kumquat.id))
+ [Kumquat-1, Kumquat-2]
+
+Actually, select() is just shorthand for select(Kumquat).
+
+ >>> print list(collection.select(Kumquat).order_by(Kumquat.id))
+ [Kumquat-1, Kumquat-2]
+
+You can also query individual columns.
+
+ >>> list(collection.select(Kumquat.id).order_by(Kumquat.id))
+ [1, 2]
+
+Since the select method returns a result set, you can even use aggregate
+functions.
+
+ >>> [int(count) for count in collection.select(Count())]
+ [2]
+
+You can refine the matching conditions using the refine method.
+Collections are immutable, so all refinements create modified copies of
+the original.
+
+ >>> one = collection.refine(Kumquat.id == 1)
+ >>> list(one.select(Kumquat.id))
+ [1]
+
+You can join in arbitrary other classes, such as Guava.
+
+ >>> ok = store.execute("CREATE TEMP TABLE Guava(id integer UNIQUE)")
+ >>> class Guava(Storm):
+ ... __storm_table__ = 'Guava'
+ ... id = Int(primary=True)
+ ... def __init__(self, id):
+ ... self.id = id
+ ... def __repr__(self):
+ ... return "Guava-%d" % self.id
+ >>> obj = store.add(Guava(1))
+ >>> obj = store.add(Guava(3))
+ >>> join = collection.joinInner(Guava, Guava.id == Kumquat.id)
+
+This includes the ability to return multiple values from the join.
+
+ >>> list(join.select(Kumquat, Guava))
+ [(Kumquat-1, Guava-1)]
+
+Outer joins work in the same way.
+
+ >>> join = collection.joinOuter(Guava, Guava.id == Kumquat.id)
+ >>> list(join.select(Kumquat, Guava).order_by(Kumquat.id))
+ [(Kumquat-1, Guava-1), (Kumquat-2, None)]
=== modified file 'lib/lp/translations/interfaces/potemplate.py'
--- lib/lp/translations/interfaces/potemplate.py 2010-06-03 16:08:33 +0000
+++ lib/lp/translations/interfaces/potemplate.py 2010-07-16 14:43:43 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
# pylint: disable-msg=E0211,E0213
@@ -706,17 +706,44 @@
Examples include `ISourcePackage`, `IDistroSeries`, and `IProductSeries`.
"""
+ has_translation_templates = Bool(
+ title=_("Does this object have any translation templates?"),
+ readonly=True)
+
has_current_translation_templates = Bool(
title=_("Does this object have current translation templates?"),
readonly=True)
+ def getTemplatesCollection():
+ """Return templates as a `TranslationTemplatesCollection`.
+
+ The collection selects all `POTemplate`s attached to the
+ translation target that implements this interface.
+ """
+
+ def getCurrentTemplatesCollection():
+ """Return `TranslationTemplatesCollection` of current templates.
+
+ A translation template is considered active when both
+ `IPOTemplate`.iscurrent and the `official_rosetta` flag for its
+ containing `Product` or `Distribution` are set to True.
+ """
+ # XXX JeroenVermeulen 2010-07-16 bug=605924: Move the
+ # official_rosetta distinction into browser code.
+
def getCurrentTranslationTemplates(just_ids=False):
"""Return an iterator over all active translation templates.
+ :param just_ids: If True, return only the `POTemplate.id` rather
+ than the full `POTemplate`. Used to save time on retrieving
+ and deserializing the objects from the database.
+
A translation template is considered active when both
- `IPOTemplate`.iscurrent and parent official_rosetta flags
- are set to True.
+ `IPOTemplate`.iscurrent and the `official_rosetta` flag for its
+ containing `Product` or `Distribution` are set to True.
"""
+ # XXX JeroenVermeulen 2010-07-16 bug=605924: Move the
+ # official_rosetta distinction into browser code.
def getCurrentTranslationFiles(just_ids=False):
"""Return an iterator over all active translation files.
@@ -745,5 +772,12 @@
"""A list of native formats for all current translation templates.
"""
+ def getTemplatesAndLanguageCounts():
+ """List tuples of `POTemplate` and its language count.
+
+ A template's language count is the number of `POFile`s that
+ exist for it.
+ """
+
# Monkey patch for circular import avoidance done in
# _schema_circular_imports.py
=== modified file 'lib/lp/translations/model/distroseries_translations_copy.py'
--- lib/lp/translations/model/distroseries_translations_copy.py 2010-06-07 02:52:21 +0000
+++ lib/lp/translations/model/distroseries_translations_copy.py 2010-07-16 14:43:43 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Functions to copy translations from parent to child distroseries."""
@@ -44,7 +44,7 @@
copier = MultiTableCopy(full_name, translation_tables, logger=logger)
# Incremental copy of updates is no longer supported
- assert len(list(child.getTranslationTemplates())) == 0, (
+ assert not child.has_translation_templates, (
"The child series must not yet have any translation templates.")
logger.info(
=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py 2010-05-27 14:38:56 +0000
+++ lib/lp/translations/model/potemplate.py 2010-07-16 14:43:43 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2010 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,6 +13,7 @@
'POTemplateSet',
'POTemplateSubset',
'POTemplateToTranslationFileDataAdapter',
+ 'TranslationTemplatesCollection',
]
import datetime
@@ -24,7 +25,7 @@
from sqlobject import (
BoolCol, ForeignKey, IntCol, SQLMultipleJoin, SQLObjectNotFound,
StringCol)
-from storm.expr import Alias, And, Desc, In, Join, LeftJoin, Or, SQL
+from storm.expr import And, Count, Desc, In, Join, LeftJoin, Or
from storm.info import ClassAlias
from storm.store import Store
from zope.component import getAdapter, getUtility
@@ -41,6 +42,7 @@
from canonical.launchpad.interfaces.lpstorm import IStore
from lp.translations.utilities.rosettastats import RosettaStats
from lp.services.database.prejoin import prejoin
+from lp.services.database.collection import Collection
from lp.services.worlddata.model.language import Language
from lp.registry.interfaces.person import validate_public_person
from lp.registry.model.sourcepackagename import SourcePackageName
@@ -90,16 +92,15 @@
'''
standardTemplateHeader = (
-"Project-Id-Version: %(origin)s\n"
-"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
-"POT-Creation-Date: %(templatedate)s\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: %(languagename)s <%(languagecode)s@xxxxxx>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-)
+ "Project-Id-Version: %(origin)s\n"
+ "Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
+ "POT-Creation-Date: %(templatedate)s\n"
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+ "Language-Team: %(languagename)s <%(languagecode)s@xxxxxx>\n"
+ "MIME-Version: 1.0\n"
+ "Content-Type: text/plain; charset=UTF-8\n"
+ "Content-Transfer-Encoding: 8bit\n")
standardPOFileHeader = (standardTemplateHeader +
@@ -1378,7 +1379,6 @@
self.sourcepackagename = sourcepackagename
self.product = product
-
def _get_potemplate_equivalence_class(self, template):
"""Return whatever we group `POTemplate`s by for sharing purposes."""
if template.sourcepackagename is None:
@@ -1520,58 +1520,150 @@
"""Helper class for implementing `IHasTranslationTemplates`."""
implements(IHasTranslationTemplates)
- def getCurrentTranslationTemplates(self, just_ids=False):
- """See `IHasTranslationTemplates`."""
- raise NotImplementedError('This must be provided when subclassing.')
+ def getTemplatesCollection(self):
+ """See `IHasTranslationTemplates`.
+
+ To be provided by derived classes.
+ """
+ raise NotImplementedError(
+ "Child class must provide getTemplatesCollection.")
+
+ def _orderTemplates(self, result):
+ """Apply the conventional ordering to a result set of templates."""
+ return result.order_by(Desc(POTemplate.priority), POTemplate.name)
+
+ def getCurrentTemplatesCollection(self, current_value=True):
+ """See `IHasTranslationTemplates`."""
+ collection = self.getTemplatesCollection()
+
+ # XXX JeroenVermeulen 2010-07-15 bug=605924: Move the
+ # official_rosetta distinction into browser code.
+ if collection.target_pillar.official_rosetta:
+ return collection.restrictCurrent(current_value)
+ else:
+ # Product/Distribution does not have translation enabled.
+ # Treat all templates as obsolete.
+ return collection.refine(not current_value)
+
+ def getCurrentTranslationTemplates(self,
+ just_ids=False,
+ current_value=True):
+ """See `IHasTranslationTemplates`."""
+ if just_ids:
+ selection = POTemplate.id
+ else:
+ selection = POTemplate
+
+ collection = self.getCurrentTemplatesCollection(current_value)
+ return self._orderTemplates(collection.select(selection))
+
+ @property
+ def has_translation_templates(self):
+ """See `IHasTranslationTemplates`."""
+ return bool(self.getTranslationTemplates().any())
@property
def has_current_translation_templates(self):
- """Does self have current translation templates?"""
- templates = self.getCurrentTranslationTemplates()
- return bool(templates.any())
+ """See `IHasTranslationTemplates`."""
+ return bool(self.getCurrentTranslationTemplates(just_ids=True).any())
def getCurrentTranslationFiles(self, just_ids=False):
"""See `IHasTranslationTemplates`."""
- # XXX 2009-06-30 Danilo: until Storm can do find() on
- # ResultSets (bug #338255), we need to manually get a select
- # and extend it with another condition.
- current_templates = (
- self.getCurrentTranslationTemplates()._get_select())
- for column in current_templates.columns:
- if column.name == 'id':
- columns = [column]
- break
- assert len(columns) == 1, (
- 'There is no ID column in getCurrentTranslationTemplates '
- 'select query.')
- current_templates.columns = columns
- templates = Alias(current_templates, 'potemplates')
-
if just_ids:
- looking_for = POFile.id
+ selection = POFile.id
else:
- looking_for = POFile
- store = Store.of(self)
- if store is None:
- # If self is a non-DB object like SourcePackage,
- # it keeps current store in self._store.
- store = self._store
- return store.using(POFile, templates).find(
- looking_for, POFile.potemplate==SQL('potemplates.id'))
+ selection = POFile
+
+ collection = self.getCurrentTemplatesCollection()
+ return collection.joinPOFile().select(selection)
def getObsoleteTranslationTemplates(self):
"""See `IHasTranslationTemplates`."""
- raise NotImplementedError('This must be provided when subclassing.')
+ # XXX JeroenVermeulen 2010-07-15 bug=605924: This returns a list
+ # whereas the analogous method for current template returns a
+ # result set. Clean up this mess.
+ return list(self.getCurrentTranslationTemplates(current_value=False))
def getTranslationTemplates(self):
"""See `IHasTranslationTemplates`."""
- raise NotImplementedError('This must be provided when subclassing.')
+ return self._orderTemplates(self.getTemplatesCollection().select())
def getTranslationTemplateFormats(self):
"""See `IHasTranslationTemplates`."""
formats_query = self.getCurrentTranslationTemplates().order_by(
'source_file_format').config(distinct=True)
- formats = helpers.shortlist(
- formats_query.values(POTemplate.source_file_format),
- 10)
- return formats
+ return helpers.shortlist(
+ formats_query.values(POTemplate.source_file_format), 10)
+
+ def getTemplatesAndLanguageCounts(self):
+ """See `IHasTranslationTemplates`."""
+ join = self.getTemplatesCollection().joinOuterPOFile()
+ result = join.select(POTemplate, Count(POFile.id))
+ return result.group_by(POTemplate)
+
+
+class TranslationTemplatesCollection(Collection):
+ """A `Collection` of `POTemplate`."""
+ starting_table = POTemplate
+
+ # The Product or Distribution that this collection is restricted to.
+ target_pillar = None
+
+ def __init__(self, *args, **kwargs):
+ super(TranslationTemplatesCollection, self).__init__(*args, **kwargs)
+ if self.base is not None:
+ self.target_pillar = self.base.target_pillar
+
+ def restrictProductSeries(self, productseries):
+ product = productseries.product
+ new_collection = self.refine(
+ POTemplate.productseriesID == productseries.id)
+ new_collection._setTargetPillar(product)
+ return new_collection
+
+ def restrictDistroSeries(self, distroseries):
+ distribution = distroseries.distribution
+ new_collection = self.refine(
+ POTemplate.distroseriesID == distroseries.id)
+ new_collection._setTargetPillar(distribution)
+ return new_collection
+
+ def restrictSourcePackageName(self, sourcepackagename):
+ return self.refine(
+ POTemplate.sourcepackagenameID == sourcepackagename.id)
+
+ def _setTargetPillar(self, target_pillar):
+ assert (
+ self.target_pillar is None or
+ self.target_pillar == target_pillar), (
+ "Collection restricted to both %s and %s." % (
+ self.target_pillar, target_pillar))
+ self.target_pillar = target_pillar
+
+ def restrictCurrent(self, current_value=True):
+ """Select based on `POTemplate.iscurrent`.
+
+ :param current_value: The value for `iscurrent` that you are
+ looking for. Defaults to True, meaning this will restrict
+ to current templates. If False, will select obsolete
+ templates instead.
+ :return: A `TranslationTemplatesCollection` based on this one,
+ but restricted to ones with the desired `iscurrent` value.
+ """
+ return self.refine(POTemplate.iscurrent == current_value)
+
+ def joinPOFile(self):
+ """Join `POFile` into the collection.
+
+ :return: A `TranslationTemplatesCollection` with an added inner
+ join to `POFile`.
+ """
+ return self.joinInner(POFile, POTemplate.id == POFile.potemplateID)
+
+ def joinOuterPOFile(self):
+ """Outer-join `POFile` into the collection.
+
+ :return: A `TranslationTemplatesCollection` with an added outer
+ join to `POFile`.
+ """
+ return self.joinOuter(POFile, POTemplate.id == POFile.potemplateID)
=== added file 'lib/lp/translations/tests/test_translationtemplatescollection.py'
--- lib/lp/translations/tests/test_translationtemplatescollection.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/tests/test_translationtemplatescollection.py 2010-07-16 14:43:43 +0000
@@ -0,0 +1,130 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test `TranslationTemplatesCollection`."""
+
+__metaclass__ = type
+
+from zope.security.proxy import removeSecurityProxy
+from canonical.testing import DatabaseFunctionalLayer
+from lp.testing import TestCaseWithFactory
+
+from lp.translations.model.pofile import POFile
+from lp.translations.model.potemplate import (
+ POTemplate,
+ TranslationTemplatesCollection)
+
+
+class TestSomething(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_baseline(self):
+ # A collection constructed with no arguments selects all
+ # templates.
+ template = self.factory.makePOTemplate()
+
+ collection = TranslationTemplatesCollection()
+ self.assertIn(template, collection.select())
+
+ def test_restrictProductSeries(self):
+ trunk = self.factory.makeProduct().getSeries('trunk')
+ template = self.factory.makePOTemplate(productseries=trunk)
+
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictProductSeries(trunk)
+
+ self.assertContentEqual([template], by_series.select())
+
+ def test_restrictDistroSeries(self):
+ package = self.factory.makeSourcePackage()
+ template = self.factory.makePOTemplate(
+ distroseries=package.distroseries,
+ sourcepackagename=package.sourcepackagename)
+
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictDistroSeries(package.distroseries)
+
+ self.assertContentEqual([template], by_series.select())
+
+ def test_restrictSourcePackageName(self):
+ package = self.factory.makeSourcePackage()
+ template = self.factory.makePOTemplate(
+ distroseries=package.distroseries,
+ sourcepackagename=package.sourcepackagename)
+
+ assert package.sourcepackagename
+ collection = TranslationTemplatesCollection()
+ by_packagename = collection.restrictSourcePackageName(
+ package.sourcepackagename)
+
+ self.assertContentEqual([template], by_packagename.select())
+
+ def test_restrict_SourcePackage(self):
+ # You can restrict to a source package by restricting both to a
+ # DistroSeries and to a SourcePackageName.
+ package = self.factory.makeSourcePackage()
+ template = self.factory.makePOTemplate(
+ distroseries=package.distroseries,
+ sourcepackagename=package.sourcepackagename)
+
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictDistroSeries(package.distroseries)
+ by_package = by_series.restrictSourcePackageName(
+ package.sourcepackagename)
+
+ self.assertContentEqual([template], by_package.select())
+
+ def test_restrictCurrent(self):
+ trunk = self.factory.makeProduct().getSeries('trunk')
+ template = self.factory.makePOTemplate(productseries=trunk)
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictProductSeries(trunk)
+
+ current_templates = by_series.restrictCurrent(True)
+ obsolete_templates = by_series.restrictCurrent(False)
+
+ removeSecurityProxy(template).iscurrent = True
+ self.assertContentEqual(
+ [template], current_templates.select())
+ self.assertContentEqual([], obsolete_templates.select())
+
+ removeSecurityProxy(template).iscurrent = False
+ self.assertContentEqual([], current_templates.select())
+ self.assertContentEqual(
+ [template], obsolete_templates.select())
+
+ def test_joinPOFile(self):
+ trunk = self.factory.makeProduct().getSeries('trunk')
+ translated_template = self.factory.makePOTemplate(productseries=trunk)
+ untranslated_template = self.factory.makePOTemplate(
+ productseries=trunk)
+ nl = translated_template.newPOFile('nl')
+ de = translated_template.newPOFile('de')
+
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictProductSeries(trunk)
+ joined = by_series.joinPOFile()
+
+ self.assertContentEqual(
+ [(translated_template, nl), (translated_template, de)],
+ joined.select(POTemplate, POFile))
+
+ def test_joinOuterPOFile(self):
+ trunk = self.factory.makeProduct().getSeries('trunk')
+ translated_template = self.factory.makePOTemplate(productseries=trunk)
+ untranslated_template = self.factory.makePOTemplate(
+ productseries=trunk)
+ nl = translated_template.newPOFile('nl')
+ de = translated_template.newPOFile('de')
+
+ collection = TranslationTemplatesCollection()
+ by_series = collection.restrictProductSeries(trunk)
+ joined = by_series.joinOuterPOFile()
+
+ expected_outcome = [
+ (translated_template, nl),
+ (translated_template, de),
+ (untranslated_template, None),
+ ]
+ self.assertContentEqual(
+ expected_outcome, joined.select(POTemplate, POFile))
Follow ups