← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:stormify-sprint into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:stormify-sprint into launchpad:master.

Commit message:
Convert Sprint and SprintSpecification to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/390578
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-sprint into launchpad:master.
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index aae704a..da94d43 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Sprint views."""
@@ -27,8 +27,8 @@ from collections import defaultdict
 import csv
 
 from lazr.restful.utils import smartquote
-import six
 import pytz
+import six
 from zope.component import getUtility
 from zope.formlib.widget import CustomWidgetFactory
 from zope.formlib.widgets import TextAreaWidget
@@ -462,6 +462,7 @@ class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
             # only a single item was selected, but we want to deal with a
             # list for the general case, so convert it to a list
             selected_specs = [selected_specs]
+        selected_specs = [int(speclink) for speclink in selected_specs]
 
         if action == 'Accepted':
             action_fn = self.context.acceptSpecificationLinks
diff --git a/lib/lp/blueprints/browser/tests/test_views.py b/lib/lp/blueprints/browser/tests/test_views.py
index 5b68b37..cbbe2e5 100644
--- a/lib/lp/blueprints/browser/tests/test_views.py
+++ b/lib/lp/blueprints/browser/tests/test_views.py
@@ -110,7 +110,8 @@ def test_suite():
     for filename in filenames:
         path = filename
         one_test = LayeredDocFileSuite(
-            path, setUp=setUp, tearDown=tearDown,
+            path,
+            setUp=lambda test: setUp(test, future=True), tearDown=tearDown,
             layer=DatabaseFunctionalLayer,
             stdout_logging_level=logging.WARNING)
         suite.addTest(one_test)
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 40cf6ab..4b37d10 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -23,14 +23,15 @@ from sqlobject import (
     SQLRelatedJoin,
     StringCol,
     )
-from storm.expr import (
+from storm.locals import (
     Count,
     Desc,
     Join,
     Or,
+    ReferenceSet,
     SQL,
+    Store,
     )
-from storm.store import Store
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -237,11 +238,13 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
         joinColumn='specification', otherColumn='person',
         intermediateTable='SpecificationSubscription',
         orderBy=['display_name', 'name'])
-    sprint_links = SQLMultipleJoin('SprintSpecification', orderBy='id',
-        joinColumn='specification')
-    sprints = SQLRelatedJoin('Sprint', orderBy='name',
-        joinColumn='specification', otherColumn='sprint',
-        intermediateTable='SprintSpecification')
+    sprint_links = ReferenceSet(
+        '<primary key>', 'SprintSpecification.specification_id',
+        order_by='SprintSpecification.id')
+    sprints = ReferenceSet(
+        '<primary key>', 'SprintSpecification.specification_id',
+        'SprintSpecification.sprint_id', 'Sprint.id',
+        order_by='Sprint.name')
     spec_dependency_links = SQLMultipleJoin('SpecificationDependency',
         joinColumn='specification', orderBy='id')
 
@@ -827,13 +830,11 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
 
     def unlinkSprint(self, sprint):
         """See ISpecification."""
-        from lp.blueprints.model.sprintspecification import (
-            SprintSpecification)
         for sprint_link in self.sprint_links:
             # sprints have unique names
             if sprint_link.sprint.name == sprint.name:
-                SprintSpecification.delete(sprint_link.id)
-                return sprint_link
+                sprint_link.destroySelf()
+                return
 
     # dependencies
     def createDependency(self, specification):
@@ -1060,8 +1061,8 @@ class SpecificationSet(HasSpecificationsMixin):
     def coming_sprints(self):
         """See ISpecificationSet."""
         from lp.blueprints.model.sprint import Sprint
-        return Sprint.select("time_ends > 'NOW'", orderBy='time_starts',
-            limit=5)
+        rows = IStore(Sprint).find(Sprint, Sprint.time_ends > UTC_NOW)
+        return rows.order_by(Sprint.time_starts).config(limit=5)
 
     def new(self, name, title, specurl, summary, definition_status,
             owner, target, approver=None, assignee=None, drafter=None,
diff --git a/lib/lp/blueprints/model/sprint.py b/lib/lp/blueprints/model/sprint.py
index 2446437..ed6748a 100644
--- a/lib/lp/blueprints/model/sprint.py
+++ b/lib/lp/blueprints/model/sprint.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -8,17 +8,17 @@ __all__ = [
     'HasSprintsMixin',
     ]
 
-
-from sqlobject import (
-    BoolCol,
-    ForeignKey,
-    StringCol,
-    )
+import pytz
 from storm.locals import (
+    Bool,
+    DateTime,
     Desc,
+    Int,
     Join,
     Or,
+    Reference,
     Store,
+    Unicode,
     )
 from zope.component import getUtility
 from zope.interface import implementer
@@ -38,7 +38,10 @@ from lp.blueprints.interfaces.sprint import (
     ISprint,
     ISprintSet,
     )
-from lp.blueprints.model.specification import HasSpecificationsMixin
+from lp.blueprints.model.specification import (
+    HasSpecificationsMixin,
+    Specification,
+    )
 from lp.blueprints.model.specificationsearch import (
     get_specification_active_product_filter,
     get_specification_filters,
@@ -51,46 +54,66 @@ from lp.registry.interfaces.person import (
     validate_public_person,
     )
 from lp.registry.model.hasdrivers import HasDriversMixin
-from lp.services.database.constants import DEFAULT
-from lp.services.database.datetimecol import UtcDateTimeCol
-from lp.services.database.sqlbase import (
-    flush_database_updates,
-    quote,
-    SQLBase,
+from lp.services.database.constants import (
+    DEFAULT,
+    UTC_NOW,
     )
+from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import flush_database_updates
+from lp.services.database.stormbase import StormBase
 from lp.services.propertycache import cachedproperty
 
 
 @implementer(ISprint, IHasLogo, IHasMugshot, IHasIcon)
-class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
+class Sprint(StormBase, HasDriversMixin, HasSpecificationsMixin):
     """See `ISprint`."""
 
-    _defaultOrder = ['name']
+    __storm_table__ = 'Sprint'
+    __storm_order__ = ['name']
 
     # db field names
-    owner = ForeignKey(
-        dbName='owner', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
-    name = StringCol(notNull=True, alternateID=True)
-    title = StringCol(notNull=True)
-    summary = StringCol(notNull=True)
-    driver = ForeignKey(
-        dbName='driver', foreignKey='Person',
-        storm_validator=validate_public_person)
-    home_page = StringCol(notNull=False, default=None)
-    homepage_content = StringCol(default=None)
-    icon = ForeignKey(
-        dbName='icon', foreignKey='LibraryFileAlias', default=None)
-    logo = ForeignKey(
-        dbName='logo', foreignKey='LibraryFileAlias', default=None)
-    mugshot = ForeignKey(
-        dbName='mugshot', foreignKey='LibraryFileAlias', default=None)
-    address = StringCol(notNull=False, default=None)
-    datecreated = UtcDateTimeCol(notNull=True, default=DEFAULT)
-    time_zone = StringCol(notNull=True)
-    time_starts = UtcDateTimeCol(notNull=True)
-    time_ends = UtcDateTimeCol(notNull=True)
-    is_physical = BoolCol(notNull=True, default=True)
+    id = Int(primary=True)
+    owner_id = Int(
+        name='owner', validator=validate_public_person, allow_none=False)
+    owner = Reference(owner_id, 'Person.id')
+    name = Unicode(allow_none=False)
+    title = Unicode(allow_none=False)
+    summary = Unicode(allow_none=False)
+    driver_id = Int(name='driver', validator=validate_public_person)
+    driver = Reference(driver_id, 'Person.id')
+    home_page = Unicode(allow_none=True, default=None)
+    homepage_content = Unicode(default=None)
+    icon_id = Int(name='icon', default=None)
+    icon = Reference(icon_id, 'LibraryFileAlias.id')
+    logo_id = Int(name='logo', default=None)
+    logo = Reference(logo_id, 'LibraryFileAlias.id')
+    mugshot_id = Int(name='mugshot', default=None)
+    mugshot = Reference(mugshot_id, 'LibraryFileAlias.id')
+    address = Unicode(allow_none=True, default=None)
+    datecreated = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    time_zone = Unicode(allow_none=False)
+    time_starts = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    time_ends = DateTime(tzinfo=pytz.UTC, allow_none=False)
+    is_physical = Bool(allow_none=False, default=True)
+
+    def __init__(self, owner, name, title, time_zone, time_starts, time_ends,
+                 summary, address=None, driver=None, home_page=None,
+                 mugshot=None, logo=None, icon=None, is_physical=True):
+        super(Sprint, self).__init__()
+        self.owner = owner
+        self.name = name
+        self.title = title
+        self.time_zone = time_zone
+        self.time_starts = time_starts
+        self.time_ends = time_ends
+        self.summary = summary
+        self.address = address
+        self.driver = driver
+        self.home_page = home_page
+        self.mugshot = mugshot
+        self.logo = logo
+        self.icon = icon
+        self.is_physical = is_physical
 
     # attributes
 
@@ -128,7 +151,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
         tables.append(Join(
             SprintSpecification,
             SprintSpecification.specification == Specification.id))
-        query.append(SprintSpecification.sprintID == self.id)
+        query.append(SprintSpecification.sprint == self)
 
         if not filter:
             # filter could be None or [] then we decide the default
@@ -209,7 +232,7 @@ class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
         context. Here we are a sprint that could cover many products and/or
         distros.
         """
-        speclink = SprintSpecification.get(speclink_id)
+        speclink = Store.of(self).get(SprintSpecification, speclink_id)
         assert (speclink.sprint.id == self.id)
         return speclink
 
@@ -303,15 +326,16 @@ class SprintSet:
 
     def __getitem__(self, name):
         """See `ISprintSet`."""
-        return Sprint.selectOneBy(name=name)
+        return IStore(Sprint).find(Sprint, name=name).one()
 
     def __iter__(self):
         """See `ISprintSet`."""
-        return iter(Sprint.select("time_ends > 'NOW'", orderBy='time_starts'))
+        return iter(IStore(Sprint).find(
+            Sprint, Sprint.time_ends > UTC_NOW).order_by(Sprint.time_starts))
 
     @property
     def all(self):
-        return Sprint.select(orderBy='-time_starts')
+        return IStore(Sprint).find(Sprint).order_by(Sprint.time_starts)
 
     def new(self, owner, name, title, time_zone, time_starts, time_ends,
             summary, address=None, driver=None, home_page=None,
@@ -329,48 +353,50 @@ class HasSprintsMixin:
     implementing IHasSprints.
     """
 
-    def _getBaseQueryAndClauseTablesForQueryingSprints(self):
-        """Return the base SQL query and the clauseTables to be used when
-        querying sprints related to this object.
+    def _getBaseClausesForQueryingSprints(self):
+        """Return the base Storm clauses to be used when querying sprints
+        related to this object.
 
         Subclasses must overwrite this method if it doesn't suit them.
         """
-        query = """
-            Specification.%s = %s
-            AND Specification.id = SprintSpecification.specification
-            AND SprintSpecification.sprint = Sprint.id
-            AND SprintSpecification.status = %s
-            """ % (self._table, self.id,
-                   quote(SprintSpecificationStatus.ACCEPTED))
-        return query, ['Specification', 'SprintSpecification']
+        try:
+            table = getattr(self, "__storm_table__")
+        except AttributeError:
+            # XXX cjwatson 2020-09-10: Remove this once all inheritors have
+            # been converted from SQLObject to Storm.
+            table = getattr(self, "_table")
+        return [
+            getattr(Specification, table.lower()) == self,
+            Specification.id == SprintSpecification.specification_id,
+            SprintSpecification.sprint == Sprint.id,
+            SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
+            ]
 
     def getSprints(self):
-        query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
-        return Sprint.select(
-            query, clauseTables=tables, orderBy='-time_starts', distinct=True)
+        clauses = self._getBaseClausesForQueryingSprints()
+        return IStore(Sprint).find(Sprint, *clauses).order_by(
+            Desc(Sprint.time_starts)).config(distinct=True)
 
     @cachedproperty
     def sprints(self):
         """See IHasSprints."""
         return list(self.getSprints())
 
-    def getComingSprings(self):
-        query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
-        query += " AND Sprint.time_ends > 'NOW'"
-        return Sprint.select(
-            query, clauseTables=tables, orderBy='time_starts',
-            distinct=True, limit=5)
+    def getComingSprints(self):
+        clauses = self._getBaseClausesForQueryingSprints()
+        clauses.append(Sprint.time_ends > UTC_NOW)
+        return IStore(Sprint).find(Sprint, *clauses).order_by(
+            Sprint.time_starts).config(distinct=True, limit=5)
 
     @cachedproperty
     def coming_sprints(self):
         """See IHasSprints."""
-        return list(self.getComingSprings())
+        return list(self.getComingSprints())
 
     @property
     def past_sprints(self):
         """See IHasSprints."""
-        query, tables = self._getBaseQueryAndClauseTablesForQueryingSprints()
-        query += " AND Sprint.time_ends <= 'NOW'"
-        return Sprint.select(
-            query, clauseTables=tables, orderBy='-time_starts',
-            distinct=True)
+        clauses = self._getBaseClausesForQueryingSprints()
+        clauses.append(Sprint.time_ends <= UTC_NOW)
+        return IStore(Sprint).find(Sprint, *clauses).order_by(
+            Desc(Sprint.time_starts)).config(distinct=True)
diff --git a/lib/lp/blueprints/model/sprintspecification.py b/lib/lp/blueprints/model/sprintspecification.py
index 46e691a..eed7649 100644
--- a/lib/lp/blueprints/model/sprintspecification.py
+++ b/lib/lp/blueprints/model/sprintspecification.py
@@ -1,13 +1,17 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 __all__ = ['SprintSpecification']
 
-from sqlobject import (
-    ForeignKey,
-    StringCol,
+import pytz
+from storm.locals import (
+    DateTime,
+    Int,
+    Reference,
+    Store,
+    Unicode,
     )
 from zope.interface import implementer
 
@@ -18,32 +22,41 @@ from lp.services.database.constants import (
     DEFAULT,
     UTC_NOW,
     )
-from lp.services.database.datetimecol import UtcDateTimeCol
-from lp.services.database.enumcol import EnumCol
-from lp.services.database.sqlbase import SQLBase
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.stormbase import StormBase
 
 
 @implementer(ISprintSpecification)
-class SprintSpecification(SQLBase):
+class SprintSpecification(StormBase):
     """A link between a sprint and a specification."""
 
-    _table = 'SprintSpecification'
+    __storm_table__ = 'SprintSpecification'
 
-    sprint = ForeignKey(dbName='sprint', foreignKey='Sprint',
-        notNull=True)
-    specification = ForeignKey(dbName='specification',
-        foreignKey='Specification', notNull=True)
-    status = EnumCol(schema=SprintSpecificationStatus, notNull=True,
+    id = Int(primary=True)
+
+    sprint_id = Int(name='sprint', allow_none=False)
+    sprint = Reference(sprint_id, 'Sprint.id')
+    specification_id = Int(name='specification', allow_none=False)
+    specification = Reference(specification_id, 'Specification.id')
+    status = DBEnum(
+        enum=SprintSpecificationStatus, allow_none=False,
         default=SprintSpecificationStatus.PROPOSED)
-    whiteboard = StringCol(notNull=False, default=None)
-    registrant = ForeignKey(
-        dbName='registrant', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
-    date_created = UtcDateTimeCol(notNull=True, default=DEFAULT)
-    decider = ForeignKey(
-        dbName='decider', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=False, default=None)
-    date_decided = UtcDateTimeCol(notNull=False, default=None)
+    whiteboard = Unicode(allow_none=True, default=None)
+    registrant_id = Int(
+        name='registrant', validator=validate_public_person, allow_none=False)
+    registrant = Reference(registrant_id, 'Person.id')
+    date_created = DateTime(tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
+    decider_id = Int(
+        name='decider', validator=validate_public_person, allow_none=True,
+        default=None)
+    decider = Reference(decider_id, 'Person.id')
+    date_decided = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
+
+    def __init__(self, sprint, specification, registrant):
+        super(SprintSpecification, self).__init__()
+        self.sprint = sprint
+        self.specification = specification
+        self.registrant = registrant
 
     @property
     def is_confirmed(self):
@@ -66,3 +79,6 @@ class SprintSpecification(SQLBase):
         self.status = SprintSpecificationStatus.DECLINED
         self.decider = decider
         self.date_decided = UTC_NOW
+
+    def destroySelf(self):
+        Store.of(self).remove(self)
diff --git a/lib/lp/blueprints/vocabularies/sprint.py b/lib/lp/blueprints/vocabularies/sprint.py
index f300b62..f98df43 100644
--- a/lib/lp/blueprints/vocabularies/sprint.py
+++ b/lib/lp/blueprints/vocabularies/sprint.py
@@ -9,21 +9,17 @@ __all__ = [
     'SprintVocabulary',
     ]
 
-
 from lp.blueprints.model.sprint import Sprint
-from lp.services.webapp.vocabulary import NamedSQLObjectVocabulary
+from lp.services.database.constants import UTC_NOW
+from lp.services.webapp.vocabulary import NamedStormVocabulary
 
 
-class FutureSprintVocabulary(NamedSQLObjectVocabulary):
+class FutureSprintVocabulary(NamedStormVocabulary):
     """A vocab of all sprints that have not yet finished."""
 
     _table = Sprint
-
-    def __iter__(self):
-        future_sprints = Sprint.select("time_ends > 'NOW'")
-        for sprint in future_sprints:
-            yield(self.toTerm(sprint))
+    _clauses = [Sprint.time_ends > UTC_NOW]
 
 
-class SprintVocabulary(NamedSQLObjectVocabulary):
+class SprintVocabulary(NamedStormVocabulary):
     _table = Sprint
diff --git a/lib/lp/registry/model/projectgroup.py b/lib/lp/registry/model/projectgroup.py
index 8a22fa8..bc6c5e5 100644
--- a/lib/lp/registry/model/projectgroup.py
+++ b/lib/lp/registry/model/projectgroup.py
@@ -50,7 +50,11 @@ from lp.blueprints.model.specification import (
     Specification,
     )
 from lp.blueprints.model.specificationsearch import search_specifications
-from lp.blueprints.model.sprint import HasSprintsMixin
+from lp.blueprints.model.sprint import (
+    HasSprintsMixin,
+    Sprint,
+    )
+from lp.blueprints.model.sprintspecification import SprintSpecification
 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
 from lp.bugs.model.bugtarget import (
     BugTargetBase,
@@ -239,15 +243,14 @@ class ProjectGroup(SQLBase, BugTargetBase, HasSpecificationsMixin,
         """ See `IProjectGroup`."""
         return not self.getBranches().is_empty()
 
-    def _getBaseQueryAndClauseTablesForQueryingSprints(self):
-        query = """
-            Product.project = %s
-            AND Specification.product = Product.id
-            AND Specification.id = SprintSpecification.specification
-            AND SprintSpecification.sprint = Sprint.id
-            AND SprintSpecification.status = %s
-            """ % sqlvalues(self, SprintSpecificationStatus.ACCEPTED)
-        return query, ['Product', 'Specification', 'SprintSpecification']
+    def _getBaseClausesForQueryingSprints(self):
+        return [
+            Product.projectgroup == self,
+            Specification.product == Product.id,
+            Specification.id == SprintSpecification.specification_id,
+            SprintSpecification.sprint == Sprint.id,
+            SprintSpecification.status == SprintSpecificationStatus.ACCEPTED,
+            ]
 
     def specifications(self, user, sort=None, quantity=None, filter=None,
                        series=None, need_people=True, need_branches=True,
diff --git a/lib/lp/services/worlddata/vocabularies.py b/lib/lp/services/worlddata/vocabularies.py
index 58d2c8e..be963f4 100644
--- a/lib/lp/services/worlddata/vocabularies.py
+++ b/lib/lp/services/worlddata/vocabularies.py
@@ -1,6 +1,8 @@
 # Copyright 2009 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __all__ = [
     'CountryNameVocabulary',
     'LanguageVocabulary',
@@ -10,6 +12,7 @@ __all__ = [
 __metaclass__ = type
 
 import pytz
+import six
 from sqlobject import SQLObjectNotFound
 from zope.interface import alsoProvides
 from zope.schema.vocabulary import (
@@ -24,7 +27,7 @@ from lp.services.worlddata.model.country import Country
 from lp.services.worlddata.model.language import Language
 
 # create a sorted list of the common time zone names, with UTC at the start
-_values = sorted(pytz.common_timezones)
+_values = sorted(six.ensure_text(tz) for tz in pytz.common_timezones)
 _values.remove('UTC')
 _values.insert(0, 'UTC')
 
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index f5ad237..c109014 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -1121,14 +1121,14 @@ class BareLaunchpadObjectFactory(ObjectFactory):
     def makeSprint(self, title=None, name=None):
         """Make a sprint."""
         if title is None:
-            title = self.getUniqueString('title')
+            title = self.getUniqueUnicode('title')
         owner = self.makePerson()
         if name is None:
-            name = self.getUniqueString('name')
+            name = self.getUniqueUnicode('name')
         time_starts = datetime(2009, 1, 1, tzinfo=pytz.UTC)
         time_ends = datetime(2009, 1, 2, tzinfo=pytz.UTC)
-        time_zone = 'UTC'
-        summary = self.getUniqueString('summary')
+        time_zone = u'UTC'
+        summary = self.getUniqueUnicode('summary')
         return getUtility(ISprintSet).new(
             owner=owner, name=name, title=title, time_zone=time_zone,
             time_starts=time_starts, time_ends=time_ends, summary=summary)