← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Commit message:
Convert MailingList and friends to Storm

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/387206
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:stormify-mailinglist into launchpad:master.
diff --git a/lib/lp/registry/model/mailinglist.py b/lib/lp/registry/model/mailinglist.py
index f7b7183..86619a0 100644
--- a/lib/lp/registry/model/mailinglist.py
+++ b/lib/lp/registry/model/mailinglist.py
@@ -19,16 +19,18 @@ from socket import getfqdn
 from string import Template
 
 from lazr.lifecycle.event import ObjectCreatedEvent
-from sqlobject import (
-    ForeignKey,
-    StringCol,
-    )
-from storm.expr import (
+import pytz
+import six
+from storm.expr import Func
+from storm.locals import (
     And,
-    Func,
+    DateTime,
+    Int,
     Join,
     Or,
+    Reference,
     Select,
+    Unicode,
     )
 from storm.info import ClassAlias
 from storm.store import Store
@@ -64,17 +66,13 @@ from lp.services.database.constants import (
     DEFAULT,
     UTC_NOW,
     )
-from lp.services.database.datetimecol import UtcDateTimeCol
 from lp.services.database.decoratedresultset import DecoratedResultSet
-from lp.services.database.enumcol import EnumCol
+from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import (
     IMasterStore,
     IStore,
     )
-from lp.services.database.sqlbase import (
-    SQLBase,
-    sqlvalues,
-    )
+from lp.services.database.stormbase import StormBase
 from lp.services.database.stormexpr import Concatenate
 from lp.services.identity.interfaces.account import AccountStatus
 from lp.services.identity.interfaces.emailaddress import (
@@ -105,38 +103,46 @@ USABLE_STATUSES = (
 
 
 @implementer(IMessageApproval)
-class MessageApproval(SQLBase):
+class MessageApproval(StormBase):
     """A held message."""
 
-    message = ForeignKey(
-        dbName='message', foreignKey='Message',
-        notNull=True)
+    __storm_table__ = 'MessageApproval'
+
+    id = Int(primary=True)
+
+    _message_id = Int(name='message', allow_none=False)
+    message = Reference(_message_id, 'Message.id')
 
-    posted_by = ForeignKey(
-        dbName='posted_by', foreignKey='Person',
-        storm_validator=validate_public_person,
-        notNull=True)
+    posted_by_id = Int(
+        name='posted_by', validator=validate_public_person, allow_none=False)
+    posted_by = Reference(posted_by_id, 'Person.id')
 
-    posted_message = ForeignKey(
-        dbName='posted_message', foreignKey='LibraryFileAlias',
-        notNull=True)
+    posted_message_id = Int(name='posted_message', allow_none=False)
+    posted_message = Reference(posted_message_id, 'LibraryFileAlias.id')
 
-    posted_date = UtcDateTimeCol(notNull=True, default=UTC_NOW)
+    posted_date = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
 
-    mailing_list = ForeignKey(
-        dbName='mailing_list', foreignKey='MailingList',
-        notNull=True)
+    mailing_list_id = Int(name='mailing_list', allow_none=False)
+    mailing_list = Reference(mailing_list_id, 'MailingList.id')
 
-    status = EnumCol(enum=PostedMessageStatus,
-                     default=PostedMessageStatus.NEW,
-                     notNull=True)
+    status = DBEnum(
+        enum=PostedMessageStatus, default=PostedMessageStatus.NEW,
+        allow_none=False)
 
-    disposed_by = ForeignKey(
-        dbName='disposed_by', foreignKey='Person',
-        storm_validator=validate_public_person,
-        default=None)
+    disposed_by_id = Int(
+        name='disposed_by', validator=validate_public_person, default=None)
+    disposed_by = Reference(disposed_by_id, 'Person.id')
 
-    disposal_date = UtcDateTimeCol(default=None)
+    disposal_date = DateTime(tzinfo=pytz.UTC, default=None)
+
+    def __init__(self, message, posted_by, posted_message, posted_date,
+                 mailing_list):
+        super(MessageApproval, self).__init__()
+        self.message = message
+        self.posted_by = posted_by
+        self.posted_message = posted_message
+        self.posted_date = posted_date
+        self.mailing_list = mailing_list
 
     @property
     def message_id(self):
@@ -175,7 +181,7 @@ class MessageApproval(SQLBase):
 
 
 @implementer(IMailingList)
-class MailingList(SQLBase):
+class MailingList(StormBase):
     """The mailing list for a team.
 
     Teams may have at most one mailing list, and a mailing list is associated
@@ -185,31 +191,39 @@ class MailingList(SQLBase):
     XMLRPC).
     """
 
-    team = ForeignKey(
-        dbName='team', foreignKey='Person',
-        notNull=True)
+    __storm_table__ = 'MailingList'
+
+    id = Int(primary=True)
+
+    team_id = Int(name='team', allow_none=False)
+    team = Reference(team_id, 'Person.id')
+
+    registrant_id = Int(
+        name='registrant', validator=validate_public_person, allow_none=False)
+    registrant = Reference(registrant_id, 'Person.id')
 
-    registrant = ForeignKey(
-        dbName='registrant', foreignKey='Person',
-        storm_validator=validate_public_person, notNull=True)
+    date_registered = DateTime(
+        tzinfo=pytz.UTC, allow_none=False, default=DEFAULT)
 
-    date_registered = UtcDateTimeCol(notNull=True, default=DEFAULT)
+    reviewer_id = Int(
+        name='reviewer', validator=validate_public_person, default=None)
+    reviewer = Reference(reviewer_id, 'Person.id')
 
-    reviewer = ForeignKey(
-        dbName='reviewer', foreignKey='Person',
-        storm_validator=validate_public_person, default=None)
+    date_reviewed = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
 
-    date_reviewed = UtcDateTimeCol(notNull=False, default=None)
+    date_activated = DateTime(tzinfo=pytz.UTC, allow_none=True, default=None)
 
-    date_activated = UtcDateTimeCol(notNull=False, default=None)
+    status = DBEnum(
+        enum=MailingListStatus, default=MailingListStatus.APPROVED,
+        allow_none=False)
 
-    status = EnumCol(enum=MailingListStatus,
-                     default=MailingListStatus.APPROVED,
-                     notNull=True)
+    _welcome_message = Unicode(default=None, name='welcome_message')
 
-    # Use a trailing underscore because SQLObject/importpedant doesn't like
-    # the typical leading underscore.
-    welcome_message_ = StringCol(default=None, dbName='welcome_message')
+    def __init__(self, team, registrant, date_registered=DEFAULT):
+        super(MailingList, self).__init__()
+        self.team = team
+        self.registrant = registrant
+        self.date_registered = date_registered
 
     @property
     def address(self):
@@ -295,7 +309,7 @@ class MailingList(SQLBase):
                 # than as a response to a user action.
                 removeSecurityProxy(email).status = (
                     EmailAddressStatus.VALIDATED)
-            assert email.personID == self.teamID, (
+            assert email.personID == self.team_id, (
                 "Email already associated with another team.")
 
     def _setAndNotifyDateActivated(self):
@@ -323,7 +337,7 @@ class MailingList(SQLBase):
         if email is not None and self.team.preferredemail is not None:
             if email.id == self.team.preferredemail.id:
                 self.team.setContactAddress(None)
-        assert email.personID == self.teamID, 'Incorrectly linked email.'
+        assert email.personID == self.team_id, 'Incorrectly linked email.'
         # Anyone with permission to deactivate a list can also set the
         # email address status to NEW.
         removeSecurityProxy(email).status = EmailAddressStatus.NEW
@@ -344,10 +358,12 @@ class MailingList(SQLBase):
         """See `IMailingList`."""
         return self.status in USABLE_STATUSES
 
-    def _get_welcome_message(self):
-        return self.welcome_message_
+    @property
+    def welcome_message(self):
+        return self._welcome_message
 
-    def _set_welcome_message(self, text):
+    @welcome_message.setter
+    def welcome_message(self, text):
         if self.status == MailingListStatus.REGISTERED:
             # Do nothing because the status does not change.  When setting the
             # welcome_message on a newly registered mailing list the XMLRPC
@@ -362,14 +378,12 @@ class MailingList(SQLBase):
             self.status = MailingListStatus.MODIFIED
         else:
             raise AssertionError('Only usable mailing lists may be modified')
-        self.welcome_message_ = text
-
-    welcome_message = property(_get_welcome_message, _set_welcome_message)
+        self._welcome_message = text
 
     def getSubscription(self, person):
         """See `IMailingList`."""
-        return MailingListSubscription.selectOneBy(person=person,
-                                                   mailing_list=self)
+        return Store.of(self).find(
+            MailingListSubscription, person=person, mailing_list=self).one()
 
     def getSubscribers(self):
         """See `IMailingList`."""
@@ -409,7 +423,7 @@ class MailingList(SQLBase):
             raise CannotUnsubscribe(
                 '%s is not a member of the mailing list: %s' %
                 (person.displayname, self.team.displayname))
-        subscription.destroySelf()
+        Store.of(subscription).remove(subscription)
 
     def changeAddress(self, person, address):
         """See `IMailingList`."""
@@ -422,10 +436,7 @@ class MailingList(SQLBase):
             raise CannotChangeSubscription(
                 '%s does not own the email address: %s' %
                 (person.displayname, address.email))
-        if address is None:
-            subscription.email_address = None
-        else:
-            subscription.email_addressID = address.id
+        subscription.email_address = address
 
     def holdMessage(self, message):
         """See `IMailingList`."""
@@ -441,10 +452,10 @@ class MailingList(SQLBase):
         """See `IMailingList`."""
         store = Store.of(self)
         clauses = [
-            MessageApproval.mailing_listID == self.id,
+            MessageApproval.mailing_list == self,
             MessageApproval.status == PostedMessageStatus.NEW,
-            MessageApproval.messageID == Message.id,
-            MessageApproval.posted_byID == Person.id
+            MessageApproval.message == Message.id,
+            MessageApproval.posted_by == Person.id
             ]
         if message_id_filter is not None:
             clauses.append(Message.rfc822msgid.is_in(message_id_filter))
@@ -533,28 +544,27 @@ class MailingListSet:
 
     def get(self, team_name):
         """See `IMailingListSet`."""
-        assert isinstance(team_name, basestring), (
-            'team_name must be a string, not %s' % type(team_name))
-        return MailingList.selectOne("""
-            MailingList.team = Person.id
-            AND Person.name = %s
-            AND Person.teamowner IS NOT NULL
-            """ % sqlvalues(team_name),
-            clauseTables=['Person'])
+        assert isinstance(team_name, six.text_type), (
+            'team_name must be a text string, not %s' % type(team_name))
+        return IStore(MailingList).find(
+            MailingList,
+            MailingList.team == Person.id,
+            Person.name == team_name,
+            Person.teamowner != None).one()
 
     def getSubscriptionsForTeams(self, person, teams):
         """See `IMailingListSet`."""
         store = IStore(MailingList)
         team_ids = set(map(operator.attrgetter("id"), teams))
         lists = dict(store.find(
-            (MailingList.teamID, MailingList.id),
-            MailingList.teamID.is_in(team_ids),
+            (MailingList.team_id, MailingList.id),
+            MailingList.team_id.is_in(team_ids),
             MailingList.status.is_in(USABLE_STATUSES)))
         subscriptions = dict(store.find(
-            (MailingListSubscription.mailing_listID,
+            (MailingListSubscription.mailing_list_id,
              MailingListSubscription.id),
             MailingListSubscription.person == person,
-            MailingListSubscription.mailing_listID.is_in(lists.values())))
+            MailingListSubscription.mailing_list_id.is_in(lists.values())))
         by_team = {}
         for team, mailing_list in lists.items():
             by_team[team] = (mailing_list, subscriptions.get(mailing_list))
@@ -585,25 +595,25 @@ class MailingListSet:
             Join(TeamParticipation, TeamParticipation.personID == Person.id),
             Join(
                 MailingListSubscription,
-                MailingListSubscription.personID == Person.id),
+                MailingListSubscription.person_id == Person.id),
             Join(
                 MailingList,
-                MailingList.id == MailingListSubscription.mailing_listID),
-            Join(Team, Team.id == MailingList.teamID),
+                MailingList.id == MailingListSubscription.mailing_list_id),
+            Join(Team, Team.id == MailingList.team_id),
             )
         team_ids, list_ids = self._getTeamIdsAndMailingListIds(team_names)
         preferred = store.using(*tables).find(
             (EmailAddress.email, Person.display_name, Team.name),
-            And(MailingListSubscription.mailing_listID.is_in(list_ids),
+            And(MailingListSubscription.mailing_list_id.is_in(list_ids),
                 TeamParticipation.teamID.is_in(team_ids),
-                MailingList.teamID == TeamParticipation.teamID,
+                MailingList.team_id == TeamParticipation.teamID,
                 MailingList.status != MailingListStatus.INACTIVE,
                 Account.status == AccountStatus.ACTIVE,
                 Or(
-                    And(MailingListSubscription.email_addressID == None,
+                    And(MailingListSubscription.email_address_id == None,
                         EmailAddress.status == EmailAddressStatus.PREFERRED),
                     EmailAddress.id ==
-                        MailingListSubscription.email_addressID),
+                        MailingListSubscription.email_address_id),
                 ))
         # Sort by team name.
         by_team = collections.defaultdict(set)
@@ -631,8 +641,8 @@ class MailingListSet:
             Join(Account, Account.id == Person.accountID),
             Join(EmailAddress, EmailAddress.personID == Person.id),
             Join(TeamParticipation, TeamParticipation.personID == Person.id),
-            Join(MailingList, MailingList.teamID == TeamParticipation.teamID),
-            Join(Team, Team.id == MailingList.teamID),
+            Join(MailingList, MailingList.team_id == TeamParticipation.teamID),
+            Join(Team, Team.id == MailingList.team_id),
             )
         team_ids, list_ids = self._getTeamIdsAndMailingListIds(team_names)
         team_members = store.using(*tables).find(
@@ -652,14 +662,14 @@ class MailingListSet:
             Person,
             Join(Account, Account.id == Person.accountID),
             Join(EmailAddress, EmailAddress.personID == Person.id),
-            Join(MessageApproval, MessageApproval.posted_byID == Person.id),
+            Join(MessageApproval, MessageApproval.posted_by_id == Person.id),
             Join(MailingList,
-                     MailingList.id == MessageApproval.mailing_listID),
-            Join(Team, Team.id == MailingList.teamID),
+                 MailingList.id == MessageApproval.mailing_list_id),
+            Join(Team, Team.id == MailingList.team_id),
             )
         approved_posters = store.using(*tables).find(
             (Team.name, Person.display_name, EmailAddress.email),
-            And(MessageApproval.mailing_listID.is_in(list_ids),
+            And(MessageApproval.mailing_list_id.is_in(list_ids),
                 MessageApproval.status.is_in(MESSAGE_APPROVAL_STATUSES),
                 EmailAddress.status.is_in(EMAIL_ADDRESS_STATUSES),
                 Account.status == AccountStatus.ACTIVE,
@@ -681,28 +691,34 @@ class MailingListSet:
     @property
     def approved_lists(self):
         """See `IMailingListSet`."""
-        return MailingList.selectBy(status=MailingListStatus.APPROVED)
+        return IStore(MailingList).find(
+            MailingList, status=MailingListStatus.APPROVED)
 
     @property
     def active_lists(self):
         """See `IMailingListSet`."""
-        return MailingList.selectBy(status=MailingListStatus.ACTIVE)
+        return IStore(MailingList).find(
+            MailingList, status=MailingListStatus.ACTIVE)
 
     @property
     def modified_lists(self):
         """See `IMailingListSet`."""
-        return MailingList.selectBy(status=MailingListStatus.MODIFIED)
+        return IStore(MailingList).find(
+            MailingList, status=MailingListStatus.MODIFIED)
 
     @property
     def deactivated_lists(self):
         """See `IMailingListSet`."""
-        return MailingList.selectBy(status=MailingListStatus.DEACTIVATING)
+        return IStore(MailingList).find(
+            MailingList, status=MailingListStatus.DEACTIVATING)
 
     @property
     def unsynchronized_lists(self):
         """See `IMailingListSet`."""
-        return MailingList.select('status IN %s' % sqlvalues(
-            (MailingListStatus.CONSTRUCTING, MailingListStatus.UPDATING)))
+        return IStore(MailingList).find(
+            MailingList,
+            MailingList.status.is_in(
+                (MailingListStatus.CONSTRUCTING, MailingListStatus.UPDATING)))
 
     def updateTeamAddresses(self, old_hostname):
         """See `IMailingListSet`."""
@@ -718,7 +734,7 @@ class MailingListSet:
         clauses = [
             EmailAddress.person == Person.id,
             Person.teamowner != None,
-            Person.id == MailingList.teamID,
+            Person.id == MailingList.team_id,
             EmailAddress.email.endswith(old_suffix),
             ]
         addresses = IMasterStore(EmailAddress).find(
@@ -730,22 +746,30 @@ class MailingListSet:
 
 
 @implementer(IMailingListSubscription)
-class MailingListSubscription(SQLBase):
+class MailingListSubscription(StormBase):
     """A mailing list subscription."""
 
-    person = ForeignKey(
-        dbName='person', foreignKey='Person',
-        storm_validator=validate_public_person,
-        notNull=True)
+    __storm_table__ = 'MailingListSubscription'
 
-    mailing_list = ForeignKey(
-        dbName='mailing_list', foreignKey='MailingList',
-        notNull=True)
+    id = Int(primary=True)
 
-    date_joined = UtcDateTimeCol(notNull=True, default=UTC_NOW)
+    person_id = Int(
+        name='person', validator=validate_public_person, allow_none=False)
+    person = Reference(person_id, 'Person.id')
 
-    email_address = ForeignKey(dbName='email_address',
-                               foreignKey='EmailAddress')
+    mailing_list_id = Int(name='mailing_list', allow_none=False)
+    mailing_list = Reference(mailing_list_id, 'MailingList.id')
+
+    date_joined = DateTime(tzinfo=pytz.UTC, allow_none=False, default=UTC_NOW)
+
+    email_address_id = Int(name='email_address')
+    email_address = Reference(email_address_id, 'EmailAddress.id')
+
+    def __init__(self, person, mailing_list, email_address):
+        super(MailingListSubscription, self).__init__()
+        self.person = person
+        self.mailing_list = mailing_list
+        self.email_address = email_address
 
     @property
     def subscribed_address(self):
@@ -764,11 +788,10 @@ class MessageApprovalSet:
 
     def getMessageByMessageID(self, message_id):
         """See `IMessageApprovalSet`."""
-        return MessageApproval.selectOne("""
-            MessageApproval.message = Message.id AND
-            Message.rfc822msgid = %s
-            """ % sqlvalues(message_id),
-            distinct=True, clauseTables=['Message'])
+        return IStore(MessageApproval).find(
+            MessageApproval,
+            MessageApproval.message == Message.id,
+            Message.rfc822msgid == message_id).config(distinct=True).one()
 
     def getHeldMessagesWithStatus(self, status):
         """See `IMessageApprovalSet`."""
diff --git a/lib/lp/registry/scripts/mlistimport.py b/lib/lp/registry/scripts/mlistimport.py
index 368cc9a..f8d5951 100644
--- a/lib/lp/registry/scripts/mlistimport.py
+++ b/lib/lp/registry/scripts/mlistimport.py
@@ -11,6 +11,7 @@ __all__ = [
 
 from email.utils import parseaddr
 
+import six
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -32,15 +33,15 @@ class Importer:
     """Perform mailing list imports for command line scripts."""
 
     def __init__(self, team_name, log=None):
-        self.team_name = team_name
-        self.team = getUtility(IPersonSet).getByName(team_name)
+        self.team_name = six.ensure_text(team_name)
+        self.team = getUtility(IPersonSet).getByName(self.team_name)
         assert self.team is not None, (
-            'No team with name: %s' % team_name)
-        self.mailing_list = getUtility(IMailingListSet).get(team_name)
+            'No team with name: %s' % self.team_name)
+        self.mailing_list = getUtility(IMailingListSet).get(self.team_name)
         assert self.mailing_list is not None, (
-            'Team has no mailing list: %s' % team_name)
+            'Team has no mailing list: %s' % self.team_name)
         assert self.mailing_list.status == MailingListStatus.ACTIVE, (
-            'Team mailing list is not active: %s' % team_name)
+            'Team mailing list is not active: %s' % self.team_name)
         if log is None:
             self.log = BufferLogger()
         else:
diff --git a/lib/lp/registry/stories/mailinglists/lifecycle.txt b/lib/lp/registry/stories/mailinglists/lifecycle.txt
index 5a37931..d18630a 100644
--- a/lib/lp/registry/stories/mailinglists/lifecycle.txt
+++ b/lib/lp/registry/stories/mailinglists/lifecycle.txt
@@ -284,7 +284,7 @@ to delete the archives of an INACTIVE list, this must be done manually.
 
     >>> from zope.component import getUtility
     >>> from lp.registry.interfaces.mailinglist import IMailingListSet
-    >>> def print_list_state(team_name='aardvarks'):
+    >>> def print_list_state(team_name=u'aardvarks'):
     ...     login('foo.bar@xxxxxxxxxxxxx')
     ...     mailing_list = getUtility(IMailingListSet).get(team_name)
     ...     print mailing_list.status.name
@@ -358,7 +358,7 @@ be purged.
     >>> def show_states(*states):
     ...     url = 'http://launchpad.test/~aardvarks/+mailinglist'
     ...     for status in states:
-    ...         set_list_state('aardvarks', status)
+    ...         set_list_state(u'aardvarks', status)
     ...         print_list_state()
     ...         admin_browser.open(url)
     ...         print purge_text(admin_browser)
@@ -367,7 +367,7 @@ be purged.
 
 A purged list acts as if it doesn't even exist.
 
-    >>> set_list_state('aardvarks', MailingListStatus.PURGED)
+    >>> set_list_state(u'aardvarks', MailingListStatus.PURGED)
     >>> print_list_state()
     PURGED
     >>> admin_browser.open('http://launchpad.test/~aardvarks/+mailinglist')
@@ -379,4 +379,4 @@ A purged list acts as if it doesn't even exist.
 
 The team owner can see that an inactive list can be reactivated or purged.
 
-    >>> set_list_state('aardvarks', MailingListStatus.INACTIVE)
+    >>> set_list_state(u'aardvarks', MailingListStatus.INACTIVE)
diff --git a/lib/lp/registry/stories/mailinglists/welcome-message.txt b/lib/lp/registry/stories/mailinglists/welcome-message.txt
index d419457..2565616 100644
--- a/lib/lp/registry/stories/mailinglists/welcome-message.txt
+++ b/lib/lp/registry/stories/mailinglists/welcome-message.txt
@@ -14,9 +14,8 @@ address.
     >>> user_browser.open('http://launchpad.test/~aardvarks')
     >>> user_browser.getLink('Configure mailing list').click()
     >>> welcome_message = user_browser.getControl('Welcome message')
-    >>> welcome_message.value
-    ''
-    >>> welcome_message.value = 'Welcome to the Aardvarks.'
+    >>> print(welcome_message.value)
+    >>> welcome_message.value = u'Welcome to the Aardvarks.'
     >>> user_browser.getControl('Save').click()
 
 Changes to the welcome message take effect as soon as Mailman can act on it.
@@ -34,14 +33,14 @@ Changes to the welcome message take effect as soon as Mailman can act on it.
 
 What if Mailman failed to apply the change?
 
-    >>> welcome_message.value = 'This change will fail to propagate.'
+    >>> welcome_message.value = u'This change will fail to propagate.'
     >>> user_browser.getControl('Save').click()
 
     # Re-fetch the mailing list, this time from the utility.
     >>> from lp.registry.interfaces.mailinglist import IMailingListSet
     >>> login('foo.bar@xxxxxxxxxxxxx')
     >>> from zope.component import getUtility
-    >>> mailing_list = getUtility(IMailingListSet).get('aardvarks')
+    >>> mailing_list = getUtility(IMailingListSet).get(u'aardvarks')
     >>> mailing_list.status
     <DBItem MailingListStatus.MODIFIED, (8) Modified>
 
diff --git a/lib/lp/registry/tests/test_mailinglist.py b/lib/lp/registry/tests/test_mailinglist.py
index 641b6b4..7b58a0a 100644
--- a/lib/lp/registry/tests/test_mailinglist.py
+++ b/lib/lp/registry/tests/test_mailinglist.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
+
 __metaclass__ = type
 __all__ = []
 
diff --git a/lib/lp/registry/tests/test_mailinglistapi.py b/lib/lp/registry/tests/test_mailinglistapi.py
index e967e3e..3503db2 100644
--- a/lib/lp/registry/tests/test_mailinglistapi.py
+++ b/lib/lp/registry/tests/test_mailinglistapi.py
@@ -3,6 +3,8 @@
 
 """Unit tests for the private MailingList API."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 __all__ = []
 
@@ -494,7 +496,7 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
                 Message-ID: <\xa9-me>
                 Date: Fri, 01 Aug 2000 01:08:59 -0000\n
                 I put \xa9 in the body.
-                """))
+                """).encode('ISO-8859-1'))
         info = self.mailinglist_api.holdMessage(
             'team', xmlrpc_client.Binary(message.as_string()))
         transaction.commit()
@@ -507,13 +509,13 @@ class MailingListAPIMessageTestCase(TestCaseWithFactory):
         finally:
             found.posted_message.close()
         self.assertEqual([
-            'From: \\xa9 me <me@xxxxxx>',
-            'To: team@xxxxxxxxxxxxxxxxxxxx',
-            'Subject: \\xa9 gremlins',
-            'Message-ID: <\\xa9-me>',
-            'Date: Fri, 01 Aug 2000 01:08:59 -0000',
-            '',
-            'I put \xa9 in the body.'], text.splitlines())
+            b'From: \\xa9 me <me@xxxxxx>',
+            b'To: team@xxxxxxxxxxxxxxxxxxxx',
+            b'Subject: \\xa9 gremlins',
+            b'Message-ID: <\\xa9-me>',
+            b'Date: Fri, 01 Aug 2000 01:08:59 -0000',
+            b'',
+            b'I put \xa9 in the body.'], text.splitlines())
 
     def test_getMessageDispositions_accept(self):
         # List moderators can approve messages.
diff --git a/lib/lp/registry/tests/test_mlists.py b/lib/lp/registry/tests/test_mlists.py
index 5125753..f7325c8 100644
--- a/lib/lp/registry/tests/test_mlists.py
+++ b/lib/lp/registry/tests/test_mlists.py
@@ -3,6 +3,8 @@
 
 """Test mailing list stuff."""
 
+from __future__ import absolute_import, print_function, unicode_literals
+
 __metaclass__ = type
 
 
@@ -16,6 +18,7 @@ from subprocess import (
 import tempfile
 import unittest
 
+import six
 import transaction
 from zope.component import getUtility
 
@@ -81,12 +84,9 @@ class BaseMailingListImportTest(unittest.TestCase):
 
     def writeFile(self, *addresses):
         # Write the addresses to import to our open temporary file.
-        out_file = open(self.filename, 'w')
-        try:
+        with open(self.filename, 'w') as out_file:
             for address in addresses:
-                print >> out_file, address
-        finally:
-            out_file.close()
+                print(address, file=out_file)
 
     def assertPeople(self, *people):
         """Assert that `people` are members of the team."""
@@ -364,7 +364,7 @@ class TestMailingListImports(BaseMailingListImportTest):
     def test_import_existing_with_nonascii_name(self):
         # Make sure that a person with a non-ascii name, who's already a
         # member of the list, gets a proper log message.
-        self.anne.display_name = u'\u1ea2nn\u1ebf P\u1ec5rs\u1ed1n'
+        self.anne.display_name = '\u1ea2nn\u1ebf P\u1ec5rs\u1ed1n'
         importer = Importer('aardvarks', self.logger)
         self.anne.join(self.team)
         self.mailing_list.subscribe(self.anne)
@@ -373,9 +373,8 @@ class TestMailingListImports(BaseMailingListImportTest):
             'bperson@xxxxxxxxxxx',
             ))
         self.assertEqual(
-            self.logger.getLogBuffer(),
-            'ERROR \xe1\xba\xa2nn\xe1\xba\xbf '
-            'P\xe1\xbb\x85rs\xe1\xbb\x91n is already subscribed '
+            six.ensure_text(self.logger.getLogBuffer()),
+            'ERROR \u1ea2nn\u1ebf P\u1ec5rs\u1ed1n is already subscribed '
             'to list Aardvarks\n'
             'INFO anne.person@xxxxxxxxxxx (anne) joined and subscribed\n'
             'INFO bperson@xxxxxxxxxxx (bart) joined and subscribed\n')
diff --git a/lib/lp/registry/vocabularies.py b/lib/lp/registry/vocabularies.py
index 80fb0fd..bd259ea 100644
--- a/lib/lp/registry/vocabularies.py
+++ b/lib/lp/registry/vocabularies.py
@@ -179,7 +179,10 @@ from lp.services.database.sqlbase import (
     SQLBase,
     sqlvalues,
     )
-from lp.services.database.stormexpr import RegexpMatch
+from lp.services.database.stormexpr import (
+    fti_search,
+    RegexpMatch,
+    )
 from lp.services.helpers import (
     ensure_unicode,
     shortlist,
@@ -1039,13 +1042,11 @@ class ActiveMailingListVocabulary(FilteredVocabularyBase):
             return getUtility(IMailingListSet).active_lists
         # The mailing list name, such as it has one, is really the name of the
         # team to which it is linked.
-        return MailingList.select("""
-            MailingList.team = Person.id
-            AND Person.fti @@ ftq(%s)
-            AND Person.teamowner IS NOT NULL
-            AND MailingList.status = %s
-            """ % sqlvalues(text, MailingListStatus.ACTIVE),
-            clauseTables=['Person'])
+        return IStore(MailingList).find(
+            MailingList.team == Person.id,
+            fti_search(Person, text),
+            Person.teamowner != None,
+            MailingList.status == MailingListStatus.ACTIVE)
 
     def searchForTerms(self, query=None, vocab_filter=None):
         """See `IHugeVocabulary`."""
diff --git a/lib/lp/services/identity/model/emailaddress.py b/lib/lp/services/identity/model/emailaddress.py
index 5251928..00697f9 100644
--- a/lib/lp/services/identity/model/emailaddress.py
+++ b/lib/lp/services/identity/model/emailaddress.py
@@ -21,6 +21,7 @@ from zope.interface import implementer
 
 from lp.app.validators.email import valid_email
 from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import IMasterStore
 from lp.services.database.sqlbase import (
     quote,
     SQLBase,
@@ -80,9 +81,10 @@ class EmailAddress(SQLBase, HasOwnerMixin):
         # XXX 2009-05-04 jamesh bug=371567: This function should not
         # be responsible for removing subscriptions, since the SSO
         # server can't write to that table.
-        for subscription in MailingListSubscription.selectBy(
-            email_address=self):
-            subscription.destroySelf()
+        store = IMasterStore(MailingListSubscription)
+        for subscription in store.find(
+                MailingListSubscription, email_address=self):
+            store.remove(subscription)
         super(EmailAddress, self).destroySelf()
 
     @property