summaryrefslogtreecommitdiff
path: root/src/mailman/model
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/model')
-rw-r--r--src/mailman/model/address.py26
-rw-r--r--src/mailman/model/autorespond.py41
-rw-r--r--src/mailman/model/bans.py29
-rw-r--r--src/mailman/model/bounce.py23
-rw-r--r--src/mailman/model/digests.py17
-rw-r--r--src/mailman/model/docs/autorespond.rst22
-rw-r--r--src/mailman/model/docs/mailinglist.rst15
-rw-r--r--src/mailman/model/docs/messagestore.rst5
-rw-r--r--src/mailman/model/docs/requests.rst20
-rw-r--r--src/mailman/model/domain.py30
-rw-r--r--src/mailman/model/language.py12
-rw-r--r--src/mailman/model/listmanager.py26
-rw-r--r--src/mailman/model/mailinglist.py432
-rw-r--r--src/mailman/model/member.py34
-rw-r--r--src/mailman/model/message.py12
-rw-r--r--src/mailman/model/messagestore.py29
-rw-r--r--src/mailman/model/mime.py15
-rw-r--r--src/mailman/model/pending.py61
-rw-r--r--src/mailman/model/preferences.py20
-rw-r--r--src/mailman/model/requests.py41
-rw-r--r--src/mailman/model/roster.py51
-rw-r--r--src/mailman/model/tests/test_listmanager.py16
-rw-r--r--src/mailman/model/tests/test_requests.py4
-rw-r--r--src/mailman/model/uid.py14
-rw-r--r--src/mailman/model/user.py47
-rw-r--r--src/mailman/model/usermanager.py18
-rw-r--r--src/mailman/model/version.py44
27 files changed, 571 insertions, 533 deletions
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index f69679210..5d1994567 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -26,7 +26,8 @@ __all__ = [
from email.utils import formataddr
-from storm.locals import DateTime, Int, Reference, Unicode
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship, backref
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
@@ -42,17 +43,20 @@ from mailman.utilities.datetime import now
class Address(Model):
"""See `IAddress`."""
- id = Int(primary=True)
- email = Unicode()
- _original = Unicode()
- display_name = Unicode()
- _verified_on = DateTime(name='verified_on')
- registered_on = DateTime()
+ __tablename__ = 'address'
- user_id = Int()
- user = Reference(user_id, 'User.id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
+ id = Column(Integer, primary_key=True)
+ email = Column(Unicode)
+ _original = Column(Unicode)
+ display_name = Column(Unicode)
+ _verified_on = Column('verified_on', DateTime)
+ registered_on = Column(DateTime)
+
+ user_id = Column(Integer, ForeignKey('user.id'), index=True)
+
+ preferences_id = Column(Integer, ForeignKey('preferences.id'), index=True)
+ preferences = relationship(
+ 'Preferences', backref=backref('address', uselist=False))
def __init__(self, email, display_name):
super(Address, self).__init__()
diff --git a/src/mailman/model/autorespond.py b/src/mailman/model/autorespond.py
index c5e736613..cfb9e017d 100644
--- a/src/mailman/model/autorespond.py
+++ b/src/mailman/model/autorespond.py
@@ -26,7 +26,8 @@ __all__ = [
]
-from storm.locals import And, Date, Desc, Int, Reference
+from sqlalchemy import Column, Date, ForeignKey, Integer, desc
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from mailman.database.model import Model
@@ -42,16 +43,18 @@ from mailman.utilities.datetime import today
class AutoResponseRecord(Model):
"""See `IAutoResponseRecord`."""
- id = Int(primary=True)
+ __tablename__ = 'autoresponserecord'
- address_id = Int()
- address = Reference(address_id, 'Address.id')
+ id = Column(Integer, primary_key=True)
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
+ address_id = Column(Integer, ForeignKey('address.id'), index=True)
+ address = relationship('Address')
- response_type = Enum(Response)
- date_sent = Date()
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
+ mailing_list = relationship('MailingList')
+
+ response_type = Column(Enum(Response))
+ date_sent = Column(Date)
def __init__(self, mailing_list, address, response_type):
self.mailing_list = mailing_list
@@ -71,12 +74,11 @@ class AutoResponseSet:
@dbconnection
def todays_count(self, store, address, response_type):
"""See `IAutoResponseSet`."""
- return store.find(
- AutoResponseRecord,
- And(AutoResponseRecord.address == address,
- AutoResponseRecord.mailing_list == self._mailing_list,
- AutoResponseRecord.response_type == response_type,
- AutoResponseRecord.date_sent == today())).count()
+ return store.query(AutoResponseRecord).filter_by(
+ address=address,
+ mailing_list=self._mailing_list,
+ response_type=response_type,
+ date_sent=today()).count()
@dbconnection
def response_sent(self, store, address, response_type):
@@ -88,10 +90,9 @@ class AutoResponseSet:
@dbconnection
def last_response(self, store, address, response_type):
"""See `IAutoResponseSet`."""
- results = store.find(
- AutoResponseRecord,
- And(AutoResponseRecord.address == address,
- AutoResponseRecord.mailing_list == self._mailing_list,
- AutoResponseRecord.response_type == response_type)
- ).order_by(Desc(AutoResponseRecord.date_sent))
+ results = store.query(AutoResponseRecord).filter_by(
+ address=address,
+ mailing_list=self._mailing_list,
+ response_type=response_type
+ ).order_by(desc(AutoResponseRecord.date_sent))
return (None if results.count() == 0 else results.first())
diff --git a/src/mailman/model/bans.py b/src/mailman/model/bans.py
index 673e8e0c1..8678fc1e7 100644
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -27,7 +27,7 @@ __all__ = [
import re
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Integer, Unicode
from zope.interface import implementer
from mailman.database.model import Model
@@ -40,9 +40,11 @@ from mailman.interfaces.bans import IBan, IBanManager
class Ban(Model):
"""See `IBan`."""
- id = Int(primary=True)
- email = Unicode()
- list_id = Unicode()
+ __tablename__ = 'ban'
+
+ id = Column(Integer, primary_key=True)
+ email = Column(Unicode)
+ list_id = Column(Unicode)
def __init__(self, email, list_id):
super(Ban, self).__init__()
@@ -62,7 +64,7 @@ class BanManager:
@dbconnection
def ban(self, store, email):
"""See `IBanManager`."""
- bans = store.find(Ban, email=email, list_id=self._list_id)
+ bans = store.query(Ban).filter_by(email=email, list_id=self._list_id)
if bans.count() == 0:
ban = Ban(email, self._list_id)
store.add(ban)
@@ -70,9 +72,10 @@ class BanManager:
@dbconnection
def unban(self, store, email):
"""See `IBanManager`."""
- ban = store.find(Ban, email=email, list_id=self._list_id).one()
+ ban = store.query(Ban).filter_by(
+ email=email, list_id=self._list_id).first()
if ban is not None:
- store.remove(ban)
+ store.delete(ban)
@dbconnection
def is_banned(self, store, email):
@@ -81,32 +84,32 @@ class BanManager:
if list_id is None:
# The client is asking for global bans. Look up bans on the
# specific email address first.
- bans = store.find(Ban, email=email, list_id=None)
+ bans = store.query(Ban).filter_by(email=email, list_id=None)
if bans.count() > 0:
return True
# And now look for global pattern bans.
- bans = store.find(Ban, list_id=None)
+ bans = store.query(Ban).filter_by(list_id=None)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
return True
else:
# This is a list-specific ban.
- bans = store.find(Ban, email=email, list_id=list_id)
+ bans = store.query(Ban).filter_by(email=email, list_id=list_id)
if bans.count() > 0:
return True
# Try global bans next.
- bans = store.find(Ban, email=email, list_id=None)
+ bans = store.query(Ban).filter_by(email=email, list_id=None)
if bans.count() > 0:
return True
# Now try specific mailing list bans, but with a pattern.
- bans = store.find(Ban, list_id=list_id)
+ bans = store.query(Ban).filter_by(list_id=list_id)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
return True
# And now try global pattern bans.
- bans = store.find(Ban, list_id=None)
+ bans = store.query(Ban).filter_by(list_id=None)
for ban in bans:
if (ban.email.startswith('^') and
re.match(ban.email, email, re.IGNORECASE) is not None):
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 134c51263..cd658052d 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -26,7 +26,8 @@ __all__ = [
]
-from storm.locals import Bool, Int, DateTime, Unicode
+
+from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
from zope.interface import implementer
from mailman.database.model import Model
@@ -42,13 +43,15 @@ from mailman.utilities.datetime import now
class BounceEvent(Model):
"""See `IBounceEvent`."""
- id = Int(primary=True)
- list_id = Unicode()
- email = Unicode()
- timestamp = DateTime()
- message_id = Unicode()
- context = Enum(BounceContext)
- processed = Bool()
+ __tablename__ = 'bounceevent'
+
+ id = Column(Integer, primary_key=True)
+ list_id = Column(Unicode)
+ email = Column(Unicode)
+ timestamp = Column(DateTime)
+ message_id = Column(Unicode)
+ context = Column(Enum(BounceContext))
+ processed = Column(Boolean)
def __init__(self, list_id, email, msg, context=None):
self.list_id = list_id
@@ -75,12 +78,12 @@ class BounceProcessor:
@dbconnection
def events(self, store):
"""See `IBounceProcessor`."""
- for event in store.find(BounceEvent):
+ for event in store.query(BounceEvent).all():
yield event
@property
@dbconnection
def unprocessed(self, store):
"""See `IBounceProcessor`."""
- for event in store.find(BounceEvent, BounceEvent.processed == False):
+ for event in store.query(BounceEvent).filter_by(processed=False):
yield event
diff --git a/src/mailman/model/digests.py b/src/mailman/model/digests.py
index 5d9f3ddd1..7bfd512b6 100644
--- a/src/mailman/model/digests.py
+++ b/src/mailman/model/digests.py
@@ -25,7 +25,8 @@ __all__ = [
]
-from storm.locals import Int, Reference
+from sqlalchemy import Column, Integer, ForeignKey
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from mailman.database.model import Model
@@ -39,15 +40,17 @@ from mailman.interfaces.member import DeliveryMode
class OneLastDigest(Model):
"""See `IOneLastDigest`."""
- id = Int(primary=True)
+ __tablename__ = 'onelastdigest'
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
+ id = Column(Integer, primary_key=True)
- address_id = Int()
- address = Reference(address_id, 'Address.id')
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+ mailing_list = relationship('MailingList')
- delivery_mode = Enum(DeliveryMode)
+ address_id = Column(Integer, ForeignKey('address.id'))
+ address = relationship('Address')
+
+ delivery_mode = Column(Enum(DeliveryMode))
def __init__(self, mailing_list, address, delivery_mode):
self.mailing_list = mailing_list
diff --git a/src/mailman/model/docs/autorespond.rst b/src/mailman/model/docs/autorespond.rst
index 6210e48cb..809de934a 100644
--- a/src/mailman/model/docs/autorespond.rst
+++ b/src/mailman/model/docs/autorespond.rst
@@ -37,34 +37,34 @@ have already been sent today.
... 'aperson@example.com')
>>> from mailman.interfaces.autorespond import Response
- >>> response_set.todays_count(address, Response.hold)
+ >>> print(response_set.todays_count(address, Response.hold))
0
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
0
Using the response set, we can record that a hold response is sent to the
address.
>>> response_set.response_sent(address, Response.hold)
- >>> response_set.todays_count(address, Response.hold)
+ >>> print(response_set.todays_count(address, Response.hold))
1
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
0
We can also record that a command response was sent.
>>> response_set.response_sent(address, Response.command)
- >>> response_set.todays_count(address, Response.hold)
+ >>> print(response_set.todays_count(address, Response.hold))
1
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
1
Let's send one more.
>>> response_set.response_sent(address, Response.command)
- >>> response_set.todays_count(address, Response.hold)
+ >>> print(response_set.todays_count(address, Response.hold))
1
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
2
Now the day flips over and all the counts reset.
@@ -73,9 +73,9 @@ Now the day flips over and all the counts reset.
>>> from mailman.utilities.datetime import factory
>>> factory.fast_forward()
- >>> response_set.todays_count(address, Response.hold)
+ >>> print(response_set.todays_count(address, Response.hold))
0
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
0
@@ -110,7 +110,7 @@ If there's been no response sent to a particular address, None is returned.
>>> address = getUtility(IUserManager).create_address(
... 'bperson@example.com')
- >>> response_set.todays_count(address, Response.command)
+ >>> print(response_set.todays_count(address, Response.command))
0
>>> print(response_set.last_response(address, Response.command))
None
diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst
index 53ba99575..3d01710c5 100644
--- a/src/mailman/model/docs/mailinglist.rst
+++ b/src/mailman/model/docs/mailinglist.rst
@@ -50,7 +50,10 @@ receive a copy of any message sent to the mailing list.
Both addresses appear on the roster of members.
- >>> for member in mlist.members.members:
+ >>> from operator import attrgetter
+ >>> sort_key = attrgetter('address.email')
+
+ >>> for member in sorted(mlist.members.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.member>
<Member: bperson@example.com on aardvark@example.com as MemberRole.member>
@@ -72,7 +75,7 @@ A Person is now both a member and an owner of the mailing list. C Person is
an owner and a moderator.
::
- >>> for member in mlist.owners.members:
+ >>> for member in sorted(mlist.owners.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
<Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
@@ -87,13 +90,13 @@ All rosters can also be accessed indirectly.
::
>>> roster = mlist.get_roster(MemberRole.member)
- >>> for member in roster.members:
+ >>> for member in sorted(roster.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.member>
<Member: bperson@example.com on aardvark@example.com as MemberRole.member>
>>> roster = mlist.get_roster(MemberRole.owner)
- >>> for member in roster.members:
+ >>> for member in sorted(roster.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
<Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
@@ -122,7 +125,7 @@ just by changing their preferred address.
>>> mlist.subscribe(user)
<Member: Dave Person <dperson@example.com> on aardvark@example.com
as MemberRole.member>
- >>> for member in mlist.members.members:
+ >>> for member in sorted(mlist.members.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.member>
<Member: bperson@example.com on aardvark@example.com as MemberRole.member>
@@ -133,7 +136,7 @@ just by changing their preferred address.
>>> new_address.verified_on = now()
>>> user.preferred_address = new_address
- >>> for member in mlist.members.members:
+ >>> for member in sorted(mlist.members.members, key=sort_key):
... print(member)
<Member: aperson@example.com on aardvark@example.com as MemberRole.member>
<Member: bperson@example.com on aardvark@example.com as MemberRole.member>
diff --git a/src/mailman/model/docs/messagestore.rst b/src/mailman/model/docs/messagestore.rst
index 4ddce7606..f2f2ca9d2 100644
--- a/src/mailman/model/docs/messagestore.rst
+++ b/src/mailman/model/docs/messagestore.rst
@@ -28,8 +28,9 @@ header, you will get an exception.
However, if the message has a ``Message-ID`` header, it can be stored.
>>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>'
- >>> message_store.add(msg)
- 'AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35'
+ >>> x_message_id_hash = message_store.add(msg)
+ >>> print(x_message_id_hash)
+ AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
>>> print(msg.as_string())
Subject: An important message
Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst
index e99cef634..1e1eba35a 100644
--- a/src/mailman/model/docs/requests.rst
+++ b/src/mailman/model/docs/requests.rst
@@ -35,7 +35,7 @@ Holding requests
The list's requests database starts out empty.
- >>> requests.count
+ >>> print(requests.count)
0
>>> dump_list(requests.held_requests)
*Empty*
@@ -68,21 +68,21 @@ Getting requests
We can see the total number of requests being held.
- >>> requests.count
+ >>> print(requests.count)
3
We can also see the number of requests being held by request type.
- >>> requests.count_of(RequestType.subscription)
+ >>> print(requests.count_of(RequestType.subscription))
1
- >>> requests.count_of(RequestType.unsubscription)
+ >>> print(requests.count_of(RequestType.unsubscription))
1
We can also see when there are multiple held requests of a particular type.
- >>> requests.hold_request(RequestType.held_message, 'hold_4')
+ >>> print(requests.hold_request(RequestType.held_message, 'hold_4'))
4
- >>> requests.count_of(RequestType.held_message)
+ >>> print(requests.count_of(RequestType.held_message))
2
We can ask the requests database for a specific request, by providing the id
@@ -132,7 +132,7 @@ Iterating over requests
To make it easier to find specific requests, the list requests can be iterated
over by type.
- >>> requests.count_of(RequestType.held_message)
+ >>> print(requests.count_of(RequestType.held_message))
3
>>> for request in requests.of_type(RequestType.held_message):
... key, data = requests.get_request(request.id)
@@ -154,10 +154,10 @@ Deleting requests
Once a specific request has been handled, it can be deleted from the requests
database.
- >>> requests.count
+ >>> print(requests.count)
5
>>> requests.delete_request(2)
- >>> requests.count
+ >>> print(requests.count)
4
Request 2 is no longer in the database.
@@ -167,5 +167,5 @@ Request 2 is no longer in the database.
>>> for request in requests.held_requests:
... requests.delete_request(request.id)
- >>> requests.count
+ >>> print(requests.count)
0
diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py
index 28e346022..8290cb755 100644
--- a/src/mailman/model/domain.py
+++ b/src/mailman/model/domain.py
@@ -26,8 +26,8 @@ __all__ = [
]
+from sqlalchemy import Column, Integer, Unicode
from urlparse import urljoin, urlparse
-from storm.locals import Int, Unicode
from zope.event import notify
from zope.interface import implementer
@@ -44,12 +44,14 @@ from mailman.model.mailinglist import MailingList
class Domain(Model):
"""Domains."""
- id = Int(primary=True)
+ __tablename__ = 'domain'
- mail_host = Unicode()
- base_url = Unicode()
- description = Unicode()
- contact_address = Unicode()
+ id = Column(Integer, primary_key=True)
+
+ mail_host = Column(Unicode) # TODO: add index?
+ base_url = Column(Unicode)
+ description = Column(Unicode)
+ contact_address = Column(Unicode)
def __init__(self, mail_host,
description=None,
@@ -92,9 +94,9 @@ class Domain(Model):
@dbconnection
def mailing_lists(self, store):
"""See `IDomain`."""
- mailing_lists = store.find(
- MailingList,
- MailingList.mail_host == self.mail_host)
+ mailing_lists = store.query(MailingList).filter(
+ MailingList.mail_host == self.mail_host
+ ).order_by(MailingList._list_id)
for mlist in mailing_lists:
yield mlist
@@ -140,14 +142,14 @@ class DomainManager:
def remove(self, store, mail_host):
domain = self[mail_host]
notify(DomainDeletingEvent(domain))
- store.remove(domain)
+ store.delete(domain)
notify(DomainDeletedEvent(mail_host))
return domain
@dbconnection
def get(self, store, mail_host, default=None):
"""See `IDomainManager`."""
- domains = store.find(Domain, mail_host=mail_host)
+ domains = store.query(Domain).filter_by(mail_host=mail_host)
if domains.count() < 1:
return default
assert domains.count() == 1, (
@@ -164,15 +166,15 @@ class DomainManager:
@dbconnection
def __len__(self, store):
- return store.find(Domain).count()
+ return store.query(Domain).count()
@dbconnection
def __iter__(self, store):
"""See `IDomainManager`."""
- for domain in store.find(Domain):
+ for domain in store.query(Domain).order_by(Domain.mail_host).all():
yield domain
@dbconnection
def __contains__(self, store, mail_host):
"""See `IDomainManager`."""
- return store.find(Domain, mail_host=mail_host).count() > 0
+ return store.query(Domain).filter_by(mail_host=mail_host).count() > 0
diff --git a/src/mailman/model/language.py b/src/mailman/model/language.py
index 14cf53f07..f4d48fc97 100644
--- a/src/mailman/model/language.py
+++ b/src/mailman/model/language.py
@@ -25,11 +25,11 @@ __all__ = [
]
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Integer, Unicode
from zope.interface import implementer
-from mailman.database import Model
-from mailman.interfaces import ILanguage
+from mailman.database.model import Model
+from mailman.interfaces.languages import ILanguage
@@ -37,5 +37,7 @@ from mailman.interfaces import ILanguage
class Language(Model):
"""See `ILanguage`."""
- id = Int(primary=True)
- code = Unicode()
+ __tablename__ = 'language'
+
+ id = Column(Integer, primary_key=True)
+ code = Column(Unicode)
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index d648a5bde..261490a92 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -52,9 +52,7 @@ class ListManager:
raise InvalidEmailAddressError(fqdn_listname)
list_id = '{0}.{1}'.format(listname, hostname)
notify(ListCreatingEvent(fqdn_listname))
- mlist = store.find(
- MailingList,
- MailingList._list_id == list_id).one()
+ mlist = store.query(MailingList).filter_by(_list_id=list_id).first()
if mlist:
raise ListAlreadyExistsError(fqdn_listname)
mlist = MailingList(fqdn_listname)
@@ -68,40 +66,41 @@ class ListManager:
"""See `IListManager`."""
listname, at, hostname = fqdn_listname.partition('@')
list_id = '{0}.{1}'.format(listname, hostname)
- return store.find(MailingList, MailingList._list_id == list_id).one()
+ return store.query(MailingList).filter_by(_list_id=list_id).first()
@dbconnection
def get_by_list_id(self, store, list_id):
"""See `IListManager`."""
- return store.find(MailingList, MailingList._list_id == list_id).one()
+ return store.query(MailingList).filter_by(_list_id=list_id).first()
@dbconnection
def delete(self, store, mlist):
"""See `IListManager`."""
fqdn_listname = mlist.fqdn_listname
notify(ListDeletingEvent(mlist))
- store.find(ContentFilter, ContentFilter.mailing_list == mlist).remove()
- store.remove(mlist)
+ store.query(ContentFilter).filter_by(mailing_list=mlist).delete()
+ store.delete(mlist)
notify(ListDeletedEvent(fqdn_listname))
@property
@dbconnection
def mailing_lists(self, store):
"""See `IListManager`."""
- for mlist in store.find(MailingList):
+ for mlist in store.query(MailingList).order_by(
+ MailingList._list_id).all():
yield mlist
@dbconnection
def __iter__(self, store):
"""See `IListManager`."""
- for mlist in store.find(MailingList):
+ for mlist in store.query(MailingList).all():
yield mlist
@property
@dbconnection
def names(self, store):
"""See `IListManager`."""
- result_set = store.find(MailingList)
+ result_set = store.query(MailingList)
for mail_host, list_name in result_set.values(MailingList.mail_host,
MailingList.list_name):
yield '{0}@{1}'.format(list_name, mail_host)
@@ -110,15 +109,16 @@ class ListManager:
@dbconnection
def list_ids(self, store):
"""See `IListManager`."""
- result_set = store.find(MailingList)
+ result_set = store.query(MailingList)
for list_id in result_set.values(MailingList._list_id):
- yield list_id
+ assert isinstance(list_id, tuple) and len(list_id) == 1
+ yield list_id[0]
@property
@dbconnection
def name_components(self, store):
"""See `IListManager`."""
- result_set = store.find(MailingList)
+ result_set = store.query(MailingList)
for mail_host, list_name in result_set.values(MailingList.mail_host,
MailingList.list_name):
yield list_name, mail_host
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 955a76968..761a78b94 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -27,9 +27,11 @@ __all__ = [
import os
-from storm.locals import (
- And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store,
- TimeDelta, Unicode)
+from sqlalchemy import (
+ Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
+ LargeBinary, PickleType, Unicode)
+from sqlalchemy.event import listen
+from sqlalchemy.orm import relationship
from urlparse import urljoin
from zope.component import getUtility
from zope.event import notify
@@ -37,6 +39,7 @@ from zope.interface import implementer
from mailman.config import config
from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.action import Action, FilterAction
from mailman.interfaces.address import IAddress
@@ -73,121 +76,121 @@ UNDERSCORE = '_'
class MailingList(Model):
"""See `IMailingList`."""
- id = Int(primary=True)
+ __tablename__ = 'mailinglist'
+
+ id = Column(Integer, primary_key=True)
# XXX denotes attributes that should be part of the public interface but
# are currently missing.
# List identity
- list_name = Unicode()
- mail_host = Unicode()
- _list_id = Unicode(name='list_id')
- allow_list_posts = Bool()
- include_rfc2369_headers = Bool()
- advertised = Bool()
- anonymous_list = Bool()
+ list_name = Column(Unicode)
+ mail_host = Column(Unicode)
+ _list_id = Column('list_id', Unicode)
+ allow_list_posts = Column(Boolean)
+ include_rfc2369_headers = Column(Boolean)
+ advertised = Column(Boolean)
+ anonymous_list = Column(Boolean)
# Attributes not directly modifiable via the web u/i
- created_at = DateTime()
+ created_at = Column(DateTime)
# Attributes which are directly modifiable via the web u/i. The more
# complicated attributes are currently stored as pickles, though that
# will change as the schema and implementation is developed.
- next_request_id = Int()
- next_digest_number = Int()
- digest_last_sent_at = DateTime()
- volume = Int()
- last_post_at = DateTime()
- # Implicit destination.
- acceptable_aliases_id = Int()
- acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id')
+ next_request_id = Column(Integer)
+ next_digest_number = Column(Integer)
+ digest_last_sent_at = Column(DateTime)
+ volume = Column(Integer)
+ last_post_at = Column(DateTime)
# Attributes which are directly modifiable via the web u/i. The more
# complicated attributes are currently stored as pickles, though that
# will change as the schema and implementation is developed.
- accept_these_nonmembers = Pickle() # XXX
- admin_immed_notify = Bool()
- admin_notify_mchanges = Bool()
- administrivia = Bool()
- archive_policy = Enum(ArchivePolicy)
+ accept_these_nonmembers = Column(PickleType) # XXX
+ admin_immed_notify = Column(Boolean)
+ admin_notify_mchanges = Column(Boolean)
+ administrivia = Column(Boolean)
+ archive_policy = Column(Enum(ArchivePolicy))
# Automatic responses.
- autoresponse_grace_period = TimeDelta()
- autorespond_owner = Enum(ResponseAction)
- autoresponse_owner_text = Unicode()
- autorespond_postings = Enum(ResponseAction)
- autoresponse_postings_text = Unicode()
- autorespond_requests = Enum(ResponseAction)
- autoresponse_request_text = Unicode()
+ autoresponse_grace_period = Column(Interval)
+ autorespond_owner = Column(Enum(ResponseAction))
+ autoresponse_owner_text = Column(Unicode)
+ autorespond_postings = Column(Enum(ResponseAction))
+ autoresponse_postings_text = Column(Unicode)
+ autorespond_requests = Column(Enum(ResponseAction))
+ autoresponse_request_text = Column(Unicode)
# Content filters.
- filter_action = Enum(FilterAction)
- filter_content = Bool()
- collapse_alternatives = Bool()
- convert_html_to_plaintext = Bool()
+ filter_action = Column(Enum(FilterAction))
+ filter_content = Column(Boolean)
+ collapse_alternatives = Column(Boolean)
+ convert_html_to_plaintext = Column(Boolean)
# Bounces.
- bounce_info_stale_after = TimeDelta() # XXX
- bounce_matching_headers = Unicode() # XXX
- bounce_notify_owner_on_disable = Bool() # XXX
- bounce_notify_owner_on_removal = Bool() # XXX
- bounce_score_threshold = Int() # XXX
- bounce_you_are_disabled_warnings = Int() # XXX
- bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX
- forward_unrecognized_bounces_to = Enum(UnrecognizedBounceDisposition)
- process_bounces = Bool()
+ bounce_info_stale_after = Column(Interval) # XXX
+ bounce_matching_headers = Column(Unicode) # XXX
+ bounce_notify_owner_on_disable = Column(Boolean) # XXX
+ bounce_notify_owner_on_removal = Column(Boolean) # XXX
+ bounce_score_threshold = Column(Integer) # XXX
+ bounce_you_are_disabled_warnings = Column(Integer) # XXX
+ bounce_you_are_disabled_warnings_interval = Column(Interval) # XXX
+ forward_unrecognized_bounces_to = Column(
+ Enum(UnrecognizedBounceDisposition))
+ process_bounces = Column(Boolean)
# Miscellaneous
- default_member_action = Enum(Action)
- default_nonmember_action = Enum(Action)
- description = Unicode()
- digest_footer_uri = Unicode()
- digest_header_uri = Unicode()
- digest_is_default = Bool()
- digest_send_periodic = Bool()
- digest_size_threshold = Float()
- digest_volume_frequency = Enum(DigestFrequency)
- digestable = Bool()
- discard_these_nonmembers = Pickle()
- emergency = Bool()
- encode_ascii_prefixes = Bool()
- first_strip_reply_to = Bool()
- footer_uri = Unicode()
- forward_auto_discards = Bool()
- gateway_to_mail = Bool()
- gateway_to_news = Bool()
- goodbye_message_uri = Unicode()
- header_matches = Pickle()
- header_uri = Unicode()
- hold_these_nonmembers = Pickle()
- info = Unicode()
- linked_newsgroup = Unicode()
- max_days_to_hold = Int()
- max_message_size = Int()
- max_num_recipients = Int()
- member_moderation_notice = Unicode()
- mime_is_default_digest = Bool()
+ default_member_action = Column(Enum(Action))
+ default_nonmember_action = Column(Enum(Action))
+ description = Column(Unicode)
+ digest_footer_uri = Column(Unicode)
+ digest_header_uri = Column(Unicode)
+ digest_is_default = Column(Boolean)
+ digest_send_periodic = Column(Boolean)
+ digest_size_threshold = Column(Float)
+ digest_volume_frequency = Column(Enum(DigestFrequency))
+ digestable = Column(Boolean)
+ discard_these_nonmembers = Column(PickleType)
+ emergency = Column(Boolean)
+ encode_ascii_prefixes = Column(Boolean)
+ first_strip_reply_to = Column(Boolean)
+ footer_uri = Column(Unicode)
+ forward_auto_discards = Column(Boolean)
+ gateway_to_mail = Column(Boolean)
+ gateway_to_news = Column(Boolean)
+ goodbye_message_uri = Column(Unicode)
+ header_matches = Column(PickleType)
+ header_uri = Column(Unicode)
+ hold_these_nonmembers = Column(PickleType)
+ info = Column(Unicode)
+ linked_newsgroup = Column(Unicode)
+ max_days_to_hold = Column(Integer)
+ max_message_size = Column(Integer)
+ max_num_recipients = Column(Integer)
+ member_moderation_notice = Column(Unicode)
+ mime_is_default_digest = Column(Boolean)
# FIXME: There should be no moderator_password
- moderator_password = RawStr()
- newsgroup_moderation = Enum(NewsgroupModeration)
- nntp_prefix_subject_too = Bool()
- nondigestable = Bool()
- nonmember_rejection_notice = Unicode()
- obscure_addresses = Bool()
- owner_chain = Unicode()
- owner_pipeline = Unicode()
- personalize = Enum(Personalization)
- post_id = Int()
- posting_chain = Unicode()
- posting_pipeline = Unicode()
- _preferred_language = Unicode(name='preferred_language')
- display_name = Unicode()
- reject_these_nonmembers = Pickle()
- reply_goes_to_list = Enum(ReplyToMunging)
- reply_to_address = Unicode()
- require_explicit_destination = Bool()
- respond_to_post_requests = Bool()
- scrub_nondigest = Bool()
- send_goodbye_message = Bool()
- send_welcome_message = Bool()
- subject_prefix = Unicode()
- topics = Pickle()
- topics_bodylines_limit = Int()
- topics_enabled = Bool()
- welcome_message_uri = Unicode()
+ moderator_password = Column(LargeBinary) # TODO : was RawStr()
+ newsgroup_moderation = Column(Enum(NewsgroupModeration))
+ nntp_prefix_subject_too = Column(Boolean)
+ nondigestable = Column(Boolean)
+ nonmember_rejection_notice = Column(Unicode)
+ obscure_addresses = Column(Boolean)
+ owner_chain = Column(Unicode)
+ owner_pipeline = Column(Unicode)
+ personalize = Column(Enum(Personalization))
+ post_id = Column(Integer)
+ posting_chain = Column(Unicode)
+ posting_pipeline = Column(Unicode)
+ _preferred_language = Column('preferred_language', Unicode)
+ display_name = Column(Unicode)
+ reject_these_nonmembers = Column(PickleType)
+ reply_goes_to_list = Column(Enum(ReplyToMunging))
+ reply_to_address = Column(Unicode)
+ require_explicit_destination = Column(Boolean)
+ respond_to_post_requests = Column(Boolean)
+ scrub_nondigest = Column(Boolean)
+ send_goodbye_message = Column(Boolean)
+ send_welcome_message = Column(Boolean)
+ subject_prefix = Column(Unicode)
+ topics = Column(PickleType)
+ topics_bodylines_limit = Column(Integer)
+ topics_enabled = Column(Boolean)
+ welcome_message_uri = Column(Unicode)
def __init__(self, fqdn_listname):
super(MailingList, self).__init__()
@@ -198,14 +201,15 @@ class MailingList(Model):
self._list_id = '{0}.{1}'.format(listname, hostname)
# For the pending database
self.next_request_id = 1
- # We need to set up the rosters. Normally, this method will get
- # called when the MailingList object is loaded from the database, but
- # that's not the case when the constructor is called. So, set up the
- # rosters explicitly.
- self.__storm_loaded__()
+ # We need to set up the rosters. Normally, this method will get called
+ # when the MailingList object is loaded from the database, but when the
+ # constructor is called, SQLAlchemy's `load` event isn't triggered.
+ # Thus we need to set up the rosters explicitly.
+ self._post_load()
makedirs(self.data_path)
- def __storm_loaded__(self):
+ def _post_load(self, *args):
+ # This hooks up to SQLAlchemy's `load` event.
self.owners = roster.OwnerRoster(self)
self.moderators = roster.ModeratorRoster(self)
self.administrators = roster.AdministratorRoster(self)
@@ -215,6 +219,13 @@ class MailingList(Model):
self.subscribers = roster.Subscribers(self)
self.nonmembers = roster.NonmemberRoster(self)
+ @classmethod
+ def __declare_last__(cls):
+ # SQLAlchemy special directive hook called after mappings are assumed
+ # to be complete. Use this to connect the roster instance creation
+ # method with the SA `load` event.
+ listen(cls, 'load', cls._post_load)
+
def __repr__(self):
return '<mailing list "{0}" at {1:#x}>'.format(
self.fqdn_listname, id(self))
@@ -323,42 +334,42 @@ class MailingList(Model):
except AttributeError:
self._preferred_language = language
- def send_one_last_digest_to(self, address, delivery_mode):
+ @dbconnection
+ def send_one_last_digest_to(self, store, address, delivery_mode):
"""See `IMailingList`."""
digest = OneLastDigest(self, address, delivery_mode)
- Store.of(self).add(digest)
+ store.add(digest)
@property
- def last_digest_recipients(self):
+ @dbconnection
+ def last_digest_recipients(self, store):
"""See `IMailingList`."""
- results = Store.of(self).find(
- OneLastDigest,
+ results = store.query(OneLastDigest).filter(
OneLastDigest.mailing_list == self)
recipients = [(digest.address, digest.delivery_mode)
for digest in results]
- results.remove()
+ results.delete()
return recipients
@property
- def filter_types(self):
+ @dbconnection
+ def filter_types(self, store):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_mime))
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_mime)
for content_filter in results:
yield content_filter.filter_pattern
@filter_types.setter
- def filter_types(self, sequence):
+ @dbconnection
+ def filter_types(self, store, sequence):
"""See `IMailingList`."""
# First, delete all existing MIME type filter patterns.
- store = Store.of(self)
- results = store.find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_mime))
- results.remove()
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_mime)
+ results.delete()
# Now add all the new filter types.
for mime_type in sequence:
content_filter = ContentFilter(
@@ -366,25 +377,24 @@ class MailingList(Model):
store.add(content_filter)
@property
- def pass_types(self):
+ @dbconnection
+ def pass_types(self, store):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_mime))
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_mime)
for content_filter in results:
yield content_filter.filter_pattern
@pass_types.setter
- def pass_types(self, sequence):
+ @dbconnection
+ def pass_types(self, store, sequence):
"""See `IMailingList`."""
# First, delete all existing MIME type pass patterns.
- store = Store.of(self)
- results = store.find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_mime))
- results.remove()
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_mime)
+ results.delete()
# Now add all the new filter types.
for mime_type in sequence:
content_filter = ContentFilter(
@@ -392,25 +402,24 @@ class MailingList(Model):
store.add(content_filter)
@property
- def filter_extensions(self):
+ @dbconnection
+ def filter_extensions(self, store):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_extension))
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_extension)
for content_filter in results:
yield content_filter.filter_pattern
@filter_extensions.setter
- def filter_extensions(self, sequence):
+ @dbconnection
+ def filter_extensions(self, store, sequence):
"""See `IMailingList`."""
# First, delete all existing file extensions filter patterns.
- store = Store.of(self)
- results = store.find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_extension))
- results.remove()
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_extension)
+ results.delete()
# Now add all the new filter types.
for mime_type in sequence:
content_filter = ContentFilter(
@@ -418,25 +427,24 @@ class MailingList(Model):
store.add(content_filter)
@property
- def pass_extensions(self):
+ @dbconnection
+ def pass_extensions(self, store):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_extension))
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_extension)
for content_filter in results:
yield content_filter.pass_pattern
@pass_extensions.setter
- def pass_extensions(self, sequence):
+ @dbconnection
+ def pass_extensions(self, store, sequence):
"""See `IMailingList`."""
# First, delete all existing file extensions pass patterns.
- store = Store.of(self)
- results = store.find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_extension))
- results.remove()
+ results = store.query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_extension)
+ results.delete()
# Now add all the new filter types.
for mime_type in sequence:
content_filter = ContentFilter(
@@ -452,29 +460,26 @@ class MailingList(Model):
elif role is MemberRole.moderator:
return self.moderators
else:
- raise TypeError(
- 'Undefined MemberRole: {0}'.format(role))
+ raise TypeError('Undefined MemberRole: {}'.format(role))
- def subscribe(self, subscriber, role=MemberRole.member):
+ @dbconnection
+ def subscribe(self, store, subscriber, role=MemberRole.member):
"""See `IMailingList`."""
- store = Store.of(self)
if IAddress.providedBy(subscriber):
- member = store.find(
- Member,
+ member = store.query(Member).filter(
Member.role == role,
Member.list_id == self._list_id,
- Member._address == subscriber).one()
+ Member._address == subscriber).first()
if member:
raise AlreadySubscribedError(
self.fqdn_listname, subscriber.email, role)
elif IUser.providedBy(subscriber):
if subscriber.preferred_address is None:
raise MissingPreferredAddressError(subscriber)
- member = store.find(
- Member,
+ member = store.query(Member).filter(
Member.role == role,
Member.list_id == self._list_id,
- Member._user == subscriber).one()
+ Member._user == subscriber).first()
if member:
raise AlreadySubscribedError(
self.fqdn_listname, subscriber, role)
@@ -494,12 +499,15 @@ class MailingList(Model):
class AcceptableAlias(Model):
"""See `IAcceptableAlias`."""
- id = Int(primary=True)
+ __tablename__ = 'acceptablealias'
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, MailingList.id)
+ id = Column(Integer, primary_key=True)
- alias = Unicode()
+ mailing_list_id = Column(
+ Integer, ForeignKey('mailinglist.id'),
+ index=True, nullable=False)
+ mailing_list = relationship('MailingList', backref='acceptable_alias')
+ alias = Column(Unicode, index=True, nullable=False)
def __init__(self, mailing_list, alias):
self.mailing_list = mailing_list
@@ -514,29 +522,30 @@ class AcceptableAliasSet:
def __init__(self, mailing_list):
self._mailing_list = mailing_list
- def clear(self):
+ @dbconnection
+ def clear(self, store):
"""See `IAcceptableAliasSet`."""
- Store.of(self._mailing_list).find(
- AcceptableAlias,
- AcceptableAlias.mailing_list == self._mailing_list).remove()
+ store.query(AcceptableAlias).filter(
+ AcceptableAlias.mailing_list == self._mailing_list).delete()
- def add(self, alias):
+ @dbconnection
+ def add(self, store, alias):
if not (alias.startswith('^') or '@' in alias):
raise ValueError(alias)
alias = AcceptableAlias(self._mailing_list, alias.lower())
- Store.of(self._mailing_list).add(alias)
+ store.add(alias)
- def remove(self, alias):
- Store.of(self._mailing_list).find(
- AcceptableAlias,
- And(AcceptableAlias.mailing_list == self._mailing_list,
- AcceptableAlias.alias == alias.lower())).remove()
+ @dbconnection
+ def remove(self, store, alias):
+ store.query(AcceptableAlias).filter(
+ AcceptableAlias.mailing_list == self._mailing_list,
+ AcceptableAlias.alias == alias.lower()).delete()
@property
- def aliases(self):
- aliases = Store.of(self._mailing_list).find(
- AcceptableAlias,
- AcceptableAlias.mailing_list == self._mailing_list)
+ @dbconnection
+ def aliases(self, store):
+ aliases = store.query(AcceptableAlias).filter(
+ AcceptableAlias.mailing_list_id == self._mailing_list.id)
for alias in aliases:
yield alias.alias
@@ -546,12 +555,17 @@ class AcceptableAliasSet:
class ListArchiver(Model):
"""See `IListArchiver`."""
- id = Int(primary=True)
+ __tablename__ = 'listarchiver'
+
+ id = Column(Integer, primary_key=True)
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, MailingList.id)
- name = Unicode()
- _is_enabled = Bool()
+ mailing_list_id = Column(
+ Integer, ForeignKey('mailinglist.id'),
+ index=True, nullable=False)
+ mailing_list = relationship('MailingList')
+
+ name = Column(Unicode, nullable=False)
+ _is_enabled = Column(Boolean)
def __init__(self, mailing_list, archiver_name, system_archiver):
self.mailing_list = mailing_list
@@ -576,32 +590,32 @@ class ListArchiver(Model):
@implementer(IListArchiverSet)
class ListArchiverSet:
- def __init__(self, mailing_list):
+ @dbconnection
+ def __init__(self, store, mailing_list):
self._mailing_list = mailing_list
system_archivers = {}
for archiver in config.archivers:
system_archivers[archiver.name] = archiver
# Add any system enabled archivers which aren't already associated
# with the mailing list.
- store = Store.of(self._mailing_list)
for archiver_name in system_archivers:
- exists = store.find(
- ListArchiver,
- And(ListArchiver.mailing_list == mailing_list,
- ListArchiver.name == archiver_name)).one()
+ exists = store.query(ListArchiver).filter(
+ ListArchiver.mailing_list == mailing_list,
+ ListArchiver.name == archiver_name).first()
if exists is None:
store.add(ListArchiver(mailing_list, archiver_name,
system_archivers[archiver_name]))
@property
- def archivers(self):
- entries = Store.of(self._mailing_list).find(
- ListArchiver, ListArchiver.mailing_list == self._mailing_list)
+ @dbconnection
+ def archivers(self, store):
+ entries = store.query(ListArchiver).filter(
+ ListArchiver.mailing_list == self._mailing_list)
for entry in entries:
yield entry
- def get(self, archiver_name):
- return Store.of(self._mailing_list).find(
- ListArchiver,
- And(ListArchiver.mailing_list == self._mailing_list,
- ListArchiver.name == archiver_name)).one()
+ @dbconnection
+ def get(self, store, archiver_name):
+ return store.query(ListArchiver).filter(
+ ListArchiver.mailing_list == self._mailing_list,
+ ListArchiver.name == archiver_name).first()
diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py
index 438796811..9da9d5d0d 100644
--- a/src/mailman/model/member.py
+++ b/src/mailman/model/member.py
@@ -24,8 +24,8 @@ __all__ = [
'Member',
]
-from storm.locals import Int, Reference, Unicode
-from storm.properties import UUID
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
@@ -33,7 +33,7 @@ from zope.interface import implementer
from mailman.core.constants import system_preferences
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
-from mailman.database.types import Enum
+from mailman.database.types import Enum, UUID
from mailman.interfaces.action import Action
from mailman.interfaces.address import IAddress
from mailman.interfaces.listmanager import IListManager
@@ -52,18 +52,20 @@ uid_factory = UniqueIDFactory(context='members')
class Member(Model):
"""See `IMember`."""
- id = Int(primary=True)
- _member_id = UUID()
- role = Enum(MemberRole)
- list_id = Unicode()
- moderation_action = Enum(Action)
+ __tablename__ = 'member'
- address_id = Int()
- _address = Reference(address_id, 'Address.id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
- user_id = Int()
- _user = Reference(user_id, 'User.id')
+ id = Column(Integer, primary_key=True)
+ _member_id = Column(UUID)
+ role = Column(Enum(MemberRole))
+ list_id = Column(Unicode)
+ moderation_action = Column(Enum(Action))
+
+ address_id = Column(Integer, ForeignKey('address.id'))
+ _address = relationship('Address')
+ preferences_id = Column(Integer, ForeignKey('preferences.id'))
+ preferences = relationship('Preferences')
+ user_id = Column(Integer, ForeignKey('user.id'))
+ _user = relationship('User')
def __init__(self, role, list_id, subscriber):
self._member_id = uid_factory.new_uid()
@@ -198,5 +200,5 @@ class Member(Model):
"""See `IMember`."""
# Yes, this must get triggered before self is deleted.
notify(UnsubscriptionEvent(self.mailing_list, self))
- store.remove(self.preferences)
- store.remove(self)
+ store.delete(self.preferences)
+ store.delete(self)
diff --git a/src/mailman/model/message.py b/src/mailman/model/message.py
index 2d697c30b..691861d46 100644
--- a/src/mailman/model/message.py
+++ b/src/mailman/model/message.py
@@ -24,7 +24,7 @@ __all__ = [
'Message',
]
-from storm.locals import AutoReload, Int, RawStr, Unicode
+from sqlalchemy import Column, Integer, LargeBinary, Unicode
from zope.interface import implementer
from mailman.database.model import Model
@@ -37,11 +37,13 @@ from mailman.interfaces.messages import IMessage
class Message(Model):
"""A message in the message store."""
- id = Int(primary=True, default=AutoReload)
- message_id = Unicode()
- message_id_hash = RawStr()
- path = RawStr()
+ __tablename__ = 'message'
+
+ id = Column(Integer, primary_key=True)
# This is a Messge-ID field representation, not a database row id.
+ message_id = Column(Unicode)
+ message_id_hash = Column(LargeBinary)
+ path = Column(LargeBinary)
@dbconnection
def __init__(self, store, message_id, message_id_hash, path):
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index a4950e8c9..19fa8133f 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -54,12 +54,13 @@ class MessageStore:
def add(self, store, message):
# Ensure that the message has the requisite headers.
message_ids = message.get_all('message-id', [])
- if len(message_ids) <> 1:
+ if len(message_ids) != 1:
raise ValueError('Exactly one Message-ID header required')
# Calculate and insert the X-Message-ID-Hash.
message_id = message_ids[0]
# Complain if the Message-ID already exists in the storage.
- existing = store.find(Message, Message.message_id == message_id).one()
+ existing = store.query(Message).filter(
+ Message.message_id == message_id).first()
if existing is not None:
raise ValueError(
'Message ID already exists in message store: {0}'.format(
@@ -80,9 +81,9 @@ class MessageStore:
# providing a unique serial number, but to get this information, we
# have to use a straight insert instead of relying on Elixir to create
# the object.
- row = Message(message_id=message_id,
- message_id_hash=hash32,
- path=relpath)
+ Message(message_id=message_id,
+ message_id_hash=hash32,
+ path=relpath)
# Now calculate the full file system path.
path = os.path.join(config.MESSAGES_DIR, relpath)
# Write the file to the path, but catch the appropriate exception in
@@ -95,7 +96,7 @@ class MessageStore:
pickle.dump(message, fp, -1)
break
except IOError as error:
- if error.errno <> errno.ENOENT:
+ if error.errno != errno.ENOENT:
raise
makedirs(os.path.dirname(path))
return hash32
@@ -107,7 +108,7 @@ class MessageStore:
@dbconnection
def get_message_by_id(self, store, message_id):
- row = store.find(Message, message_id=message_id).one()
+ row = store.query(Message).filter_by(message_id=message_id).first()
if row is None:
return None
return self._get_message(row)
@@ -116,11 +117,11 @@ class MessageStore:
def get_message_by_hash(self, store, message_id_hash):
# It's possible the hash came from a message header, in which case it
# will be a Unicode. However when coming from source code, it may be
- # an 8-string. Coerce to the latter if necessary; it must be
- # US-ASCII.
- if isinstance(message_id_hash, unicode):
+ # bytes object. Coerce to the latter if necessary; it must be ASCII.
+ if not isinstance(message_id_hash, bytes):
message_id_hash = message_id_hash.encode('ascii')
- row = store.find(Message, message_id_hash=message_id_hash).one()
+ row = store.query(Message).filter_by(
+ message_id_hash=message_id_hash).first()
if row is None:
return None
return self._get_message(row)
@@ -128,14 +129,14 @@ class MessageStore:
@property
@dbconnection
def messages(self, store):
- for row in store.find(Message):
+ for row in store.query(Message).all():
yield self._get_message(row)
@dbconnection
def delete_message(self, store, message_id):
- row = store.find(Message, message_id=message_id).one()
+ row = store.query(Message).filter_by(message_id=message_id).first()
if row is None:
raise LookupError(message_id)
path = os.path.join(config.MESSAGES_DIR, row.path)
os.remove(path)
- store.remove(row)
+ store.delete(row)
diff --git a/src/mailman/model/mime.py b/src/mailman/model/mime.py
index 570112a97..dc6a54437 100644
--- a/src/mailman/model/mime.py
+++ b/src/mailman/model/mime.py
@@ -25,7 +25,8 @@ __all__ = [
]
-from storm.locals import Int, Reference, Unicode
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from mailman.database.model import Model
@@ -38,13 +39,15 @@ from mailman.interfaces.mime import IContentFilter, FilterType
class ContentFilter(Model):
"""A single filter criteria."""
- id = Int(primary=True)
+ __tablename__ = 'contentfilter'
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
+ id = Column(Integer, primary_key=True)
- filter_type = Enum(FilterType)
- filter_pattern = Unicode()
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
+ mailing_list = relationship('MailingList')
+
+ filter_type = Column(Enum(FilterType))
+ filter_pattern = Column(Unicode)
def __init__(self, mailing_list, filter_pattern, filter_type):
self.mailing_list = mailing_list
diff --git a/src/mailman/model/pending.py b/src/mailman/model/pending.py
index 17513015c..49b12c16a 100644
--- a/src/mailman/model/pending.py
+++ b/src/mailman/model/pending.py
@@ -31,7 +31,9 @@ import random
import hashlib
from lazr.config import as_timedelta
-from storm.locals import DateTime, Int, RawStr, ReferenceSet, Unicode
+from sqlalchemy import (
+ Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from zope.interface.verify import verifyObject
@@ -49,31 +51,35 @@ from mailman.utilities.modules import call_name
class PendedKeyValue(Model):
"""A pended key/value pair, tied to a token."""
+ __tablename__ = 'pendedkeyvalue'
+
+ id = Column(Integer, primary_key=True)
+ key = Column(Unicode)
+ value = Column(Unicode)
+ pended_id = Column(Integer, ForeignKey('pended.id'), index=True)
+
def __init__(self, key, value):
self.key = key
self.value = value
- id = Int(primary=True)
- key = Unicode()
- value = Unicode()
- pended_id = Int()
-
@implementer(IPended)
class Pended(Model):
"""A pended event, tied to a token."""
+ __tablename__ = 'pended'
+
+ id = Column(Integer, primary_key=True)
+ token = Column(LargeBinary)
+ expiration_date = Column(DateTime)
+ key_values = relationship('PendedKeyValue')
+
def __init__(self, token, expiration_date):
super(Pended, self).__init__()
self.token = token
self.expiration_date = expiration_date
- id = Int(primary=True)
- token = RawStr()
- expiration_date = DateTime()
- key_values = ReferenceSet(id, PendedKeyValue.pended_id)
-
@implementer(IPendable)
@@ -105,7 +111,7 @@ class Pendings:
token = hashlib.sha1(repr(x)).hexdigest()
# In practice, we'll never get a duplicate, but we'll be anal
# about checking anyway.
- if store.find(Pended, token=token).count() == 0:
+ if store.query(Pended).filter_by(token=token).count() == 0:
break
else:
raise AssertionError('Could not find a valid pendings token')
@@ -114,10 +120,10 @@ class Pendings:
token=token,
expiration_date=now() + lifetime)
for key, value in pendable.items():
- if isinstance(key, str):
- key = unicode(key, 'utf-8')
- if isinstance(value, str):
- value = unicode(value, 'utf-8')
+ if isinstance(key, bytes):
+ key = key.decode('utf-8')
+ if isinstance(value, bytes):
+ value = value.decode('utf-8')
elif type(value) is int:
value = '__builtin__.int\1%s' % value
elif type(value) is float:
@@ -129,7 +135,7 @@ class Pendings:
value = ('mailman.model.pending.unpack_list\1' +
'\2'.join(value))
keyval = PendedKeyValue(key=key, value=value)
- pending.key_values.add(keyval)
+ pending.key_values.append(keyval)
store.add(pending)
return token
@@ -137,7 +143,7 @@ class Pendings:
def confirm(self, store, token, expunge=True):
# Token can come in as a unicode, but it's stored in the database as
# bytes. They must be ascii.
- pendings = store.find(Pended, token=str(token))
+ pendings = store.query(Pended).filter_by(token=str(token))
if pendings.count() == 0:
return None
assert pendings.count() == 1, (
@@ -146,31 +152,32 @@ class Pendings:
pendable = UnpendedPendable()
# Find all PendedKeyValue entries that are associated with the pending
# object's ID. Watch out for type conversions.
- for keyvalue in store.find(PendedKeyValue,
- PendedKeyValue.pended_id == pending.id):
+ entries = store.query(PendedKeyValue).filter(
+ PendedKeyValue.pended_id == pending.id)
+ for keyvalue in entries:
if keyvalue.value is not None and '\1' in keyvalue.value:
type_name, value = keyvalue.value.split('\1', 1)
pendable[keyvalue.key] = call_name(type_name, value)
else:
pendable[keyvalue.key] = keyvalue.value
if expunge:
- store.remove(keyvalue)
+ store.delete(keyvalue)
if expunge:
- store.remove(pending)
+ store.delete(pending)
return pendable
@dbconnection
def evict(self, store):
right_now = now()
- for pending in store.find(Pended):
+ for pending in store.query(Pended).all():
if pending.expiration_date < right_now:
# Find all PendedKeyValue entries that are associated with the
# pending object's ID.
- q = store.find(PendedKeyValue,
- PendedKeyValue.pended_id == pending.id)
+ q = store.query(PendedKeyValue).filter(
+ PendedKeyValue.pended_id == pending.id)
for keyvalue in q:
- store.remove(keyvalue)
- store.remove(pending)
+ store.delete(keyvalue)
+ store.delete(pending)
diff --git a/src/mailman/model/preferences.py b/src/mailman/model/preferences.py
index 83271d7d6..1278f80b7 100644
--- a/src/mailman/model/preferences.py
+++ b/src/mailman/model/preferences.py
@@ -25,7 +25,7 @@ __all__ = [
]
-from storm.locals import Bool, Int, Unicode
+from sqlalchemy import Boolean, Column, Integer, Unicode
from zope.component import getUtility
from zope.interface import implementer
@@ -41,14 +41,16 @@ from mailman.interfaces.preferences import IPreferences
class Preferences(Model):
"""See `IPreferences`."""
- id = Int(primary=True)
- acknowledge_posts = Bool()
- hide_address = Bool()
- _preferred_language = Unicode(name='preferred_language')
- receive_list_copy = Bool()
- receive_own_postings = Bool()
- delivery_mode = Enum(DeliveryMode)
- delivery_status = Enum(DeliveryStatus)
+ __tablename__ = 'preferences'
+
+ id = Column(Integer, primary_key=True)
+ acknowledge_posts = Column(Boolean)
+ hide_address = Column(Boolean)
+ _preferred_language = Column('preferred_language', Unicode)
+ receive_list_copy = Column(Boolean)
+ receive_own_postings = Column(Boolean)
+ delivery_mode = Column(Enum(DeliveryMode))
+ delivery_status = Column(Enum(DeliveryStatus))
def __repr__(self):
return '<Preferences object at {0:#x}>'.format(id(self))
diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py
index f3ad54797..6b130196d 100644
--- a/src/mailman/model/requests.py
+++ b/src/mailman/model/requests.py
@@ -26,7 +26,8 @@ __all__ = [
from cPickle import dumps, loads
from datetime import timedelta
-from storm.locals import AutoReload, Int, RawStr, Reference, Unicode
+from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Unicode
+from sqlalchemy.orm import relationship
from zope.component import getUtility
from zope.interface import implementer
@@ -68,25 +69,25 @@ class ListRequests:
@property
@dbconnection
def count(self, store):
- return store.find(_Request, mailing_list=self.mailing_list).count()
+ return store.query(_Request).filter_by(
+ mailing_list=self.mailing_list).count()
@dbconnection
def count_of(self, store, request_type):
- return store.find(
- _Request,
+ return store.query(_Request).filter_by(
mailing_list=self.mailing_list, request_type=request_type).count()
@property
@dbconnection
def held_requests(self, store):
- results = store.find(_Request, mailing_list=self.mailing_list)
+ results = store.query(_Request).filter_by(
+ mailing_list=self.mailing_list)
for request in results:
yield request
@dbconnection
def of_type(self, store, request_type):
- results = store.find(
- _Request,
+ results = store.query(_Request).filter_by(
mailing_list=self.mailing_list, request_type=request_type)
for request in results:
yield request
@@ -104,11 +105,15 @@ class ListRequests:
data_hash = token
request = _Request(key, request_type, self.mailing_list, data_hash)
store.add(request)
+ # XXX The caller needs a valid id immediately, so flush the changes
+ # now to the SA transaction context. Otherwise .id would not be
+ # valid. Hopefully this has no unintended side-effects.
+ store.flush()
return request.id
@dbconnection
def get_request(self, store, request_id, request_type=None):
- result = store.get(_Request, request_id)
+ result = store.query(_Request).get(request_id)
if result is None:
return None
if request_type is not None and result.request_type != request_type:
@@ -117,6 +122,8 @@ class ListRequests:
return result.key, None
pendable = getUtility(IPendings).confirm(
result.data_hash, expunge=False)
+ if pendable is None:
+ return None
data = dict()
# Unpickle any non-Unicode values.
for key, value in pendable.items():
@@ -130,25 +137,27 @@ class ListRequests:
@dbconnection
def delete_request(self, store, request_id):
- request = store.get(_Request, request_id)
+ request = store.query(_Request).get(request_id)
if request is None:
raise KeyError(request_id)
# Throw away the pended data.
getUtility(IPendings).confirm(request.data_hash)
- store.remove(request)
+ store.delete(request)
class _Request(Model):
"""Table for mailing list hold requests."""
- id = Int(primary=True, default=AutoReload)
- key = Unicode()
- request_type = Enum(RequestType)
- data_hash = RawStr()
+ __tablename__ = '_request'
+
+ id = Column(Integer, primary_key=True)
+ key = Column(Unicode)
+ request_type = Column(Enum(RequestType))
+ data_hash = Column(LargeBinary)
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
+ mailing_list = relationship('MailingList')
def __init__(self, key, request_type, mailing_list, data_hash):
super(_Request, self).__init__()
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 5a6a13269..54bc11617 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -37,7 +37,7 @@ __all__ = [
]
-from storm.expr import And, Or
+from sqlalchemy import and_, or_
from zope.interface import implementer
from mailman.database.transaction import dbconnection
@@ -65,8 +65,7 @@ class AbstractRoster:
@dbconnection
def _query(self, store):
- return store.find(
- Member,
+ return store.query(Member).filter(
Member.list_id == self._mlist.list_id,
Member.role == self.role)
@@ -104,8 +103,7 @@ class AbstractRoster:
@dbconnection
def get_member(self, store, address):
"""See `IRoster`."""
- results = store.find(
- Member,
+ results = store.query(Member).filter(
Member.list_id == self._mlist.list_id,
Member.role == self.role,
Address.email == address,
@@ -160,22 +158,20 @@ class AdministratorRoster(AbstractRoster):
@dbconnection
def _query(self, store):
- return store.find(
- Member,
+ return store.query(Member).filter(
Member.list_id == self._mlist.list_id,
- Or(Member.role == MemberRole.owner,
- Member.role == MemberRole.moderator))
+ or_(Member.role == MemberRole.owner,
+ Member.role == MemberRole.moderator))
@dbconnection
def get_member(self, store, address):
"""See `IRoster`."""
- results = store.find(
- Member,
- Member.list_id == self._mlist.list_id,
- Or(Member.role == MemberRole.moderator,
- Member.role == MemberRole.owner),
- Address.email == address,
- Member.address_id == Address.id)
+ results = store.query(Member).filter(
+ Member.list_id == self._mlist.list_id,
+ or_(Member.role == MemberRole.moderator,
+ Member.role == MemberRole.owner),
+ Address.email == address,
+ Member.address_id == Address.id)
if results.count() == 0:
return None
elif results.count() == 1:
@@ -206,10 +202,9 @@ class DeliveryMemberRoster(AbstractRoster):
:return: A generator of members.
:rtype: generator
"""
- results = store.find(
- Member,
- And(Member.list_id == self._mlist.list_id,
- Member.role == MemberRole.member))
+ results = store.query(Member).filter_by(
+ list_id = self._mlist.list_id,
+ role = MemberRole.member)
for member in results:
if member.delivery_mode in delivery_modes:
yield member
@@ -250,7 +245,7 @@ class Subscribers(AbstractRoster):
@dbconnection
def _query(self, store):
- return store.find(Member, Member.list_id == self._mlist.list_id)
+ return store.query(Member).filter_by(list_id = self._mlist.list_id)
@@ -265,12 +260,11 @@ class Memberships:
@dbconnection
def _query(self, store):
- results = store.find(
- Member,
- Or(Member.user_id == self._user.id,
- And(Address.user_id == self._user.id,
- Member.address_id == Address.id)))
- return results.config(distinct=True)
+ results = store.query(Member).filter(
+ or_(Member.user_id == self._user.id,
+ and_(Address.user_id == self._user.id,
+ Member.address_id == Address.id)))
+ return results.distinct()
@property
def member_count(self):
@@ -297,8 +291,7 @@ class Memberships:
@dbconnection
def get_member(self, store, address):
"""See `IRoster`."""
- results = store.find(
- Member,
+ results = store.query(Member).filter(
Member.address_id == Address.id,
Address.user_id == self._user.id)
if results.count() == 0:
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
index 2d3a4e3dc..b290138f3 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -29,11 +29,11 @@ __all__ = [
import unittest
-from storm.locals import Store
from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message
+from mailman.config import config
from mailman.interfaces.listmanager import (
IListManager, ListCreatedEvent, ListCreatingEvent, ListDeletedEvent,
ListDeletingEvent)
@@ -80,6 +80,15 @@ class TestListManager(unittest.TestCase):
self.assertTrue(isinstance(self._events[1], ListDeletedEvent))
self.assertEqual(self._events[1].fqdn_listname, 'another@example.com')
+ def test_list_manager_list_ids(self):
+ # You can get all the list ids for all the existing mailing lists.
+ create_list('ant@example.com')
+ create_list('bee@example.com')
+ create_list('cat@example.com')
+ self.assertEqual(
+ sorted(getUtility(IListManager).list_ids),
+ ['ant.example.com', 'bee.example.com', 'cat.example.com'])
+
class TestListLifecycleEvents(unittest.TestCase):
@@ -139,9 +148,8 @@ Message-ID: <argon>
for name in filter_names:
setattr(self._ant, name, ['test-filter-1', 'test-filter-2'])
getUtility(IListManager).delete(self._ant)
- store = Store.of(self._ant)
- filters = store.find(ContentFilter,
- ContentFilter.mailing_list == self._ant)
+ filters = config.db.store.query(ContentFilter).filter_by(
+ mailing_list = self._ant)
self.assertEqual(filters.count(), 0)
diff --git a/src/mailman/model/tests/test_requests.py b/src/mailman/model/tests/test_requests.py
index dc1b9b849..419c6077f 100644
--- a/src/mailman/model/tests/test_requests.py
+++ b/src/mailman/model/tests/test_requests.py
@@ -70,10 +70,10 @@ Something else.
# Calling hold_request() with a bogus request type is an error.
with self.assertRaises(TypeError) as cm:
self._requests_db.hold_request(5, 'foo')
- self.assertEqual(cm.exception.message, 5)
+ self.assertEqual(cm.exception.args[0], 5)
def test_delete_missing_request(self):
# Trying to delete a missing request is an error.
with self.assertRaises(KeyError) as cm:
self._requests_db.delete_request(801)
- self.assertEqual(cm.exception.message, 801)
+ self.assertEqual(cm.exception.args[0], 801)
diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py
index c60d0f1eb..72ddd7b5a 100644
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -25,11 +25,12 @@ __all__ = [
]
-from storm.locals import Int
-from storm.properties import UUID
+
+from sqlalchemy import Column, Integer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
+from mailman.database.types import UUID
@@ -45,8 +46,11 @@ class UID(Model):
There is no interface for this class, because it's purely an internal
implementation detail.
"""
- id = Int(primary=True)
- uid = UUID()
+
+ __tablename__ = 'uid'
+
+ id = Column(Integer, primary_key=True)
+ uid = Column(UUID, index=True)
@dbconnection
def __init__(self, store, uid):
@@ -70,7 +74,7 @@ class UID(Model):
:type uid: unicode
:raises ValueError: if the id is not unique.
"""
- existing = store.find(UID, uid=uid)
+ existing = store.query(UID).filter_by(uid=uid)
if existing.count() != 0:
raise ValueError(uid)
return UID(uid)
diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py
index f2c09c626..ab581fdc8 100644
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -24,14 +24,15 @@ __all__ = [
'User',
]
-from storm.locals import (
- DateTime, Int, RawStr, Reference, ReferenceSet, Unicode)
-from storm.properties import UUID
+from sqlalchemy import (
+ Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
+from sqlalchemy.orm import relationship, backref
from zope.event import notify
from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
+from mailman.database.types import UUID
from mailman.interfaces.address import (
AddressAlreadyLinkedError, AddressNotLinkedError)
from mailman.interfaces.user import (
@@ -51,24 +52,38 @@ uid_factory = UniqueIDFactory(context='users')
class User(Model):
"""Mailman users."""
- id = Int(primary=True)
- display_name = Unicode()
- _password = RawStr(name='password')
- _user_id = UUID()
- _created_on = DateTime()
+ __tablename__ = 'user'
- addresses = ReferenceSet(id, 'Address.user_id')
- _preferred_address_id = Int()
- _preferred_address = Reference(_preferred_address_id, 'Address.id')
- preferences_id = Int()
- preferences = Reference(preferences_id, 'Preferences.id')
+ id = Column(Integer, primary_key=True)
+ display_name = Column(Unicode)
+ _password = Column('password', LargeBinary)
+ _user_id = Column(UUID, index=True)
+ _created_on = Column(DateTime)
+
+ addresses = relationship(
+ 'Address', backref='user',
+ primaryjoin=(id==Address.user_id))
+
+ _preferred_address_id = Column(
+ Integer,
+ ForeignKey('address.id', use_alter=True,
+ name='_preferred_address',
+ ondelete='SET NULL'))
+
+ _preferred_address = relationship(
+ 'Address', primaryjoin=(_preferred_address_id==Address.id),
+ post_update=True)
+
+ preferences_id = Column(Integer, ForeignKey('preferences.id'), index=True)
+ preferences = relationship(
+ 'Preferences', backref=backref('user', uselist=False))
@dbconnection
def __init__(self, store, display_name=None, preferences=None):
super(User, self).__init__()
self._created_on = date_factory.now()
user_id = uid_factory.new_uid()
- assert store.find(User, _user_id=user_id).count() == 0, (
+ assert store.query(User).filter_by(_user_id=user_id).count() == 0, (
'Duplicate user id {0}'.format(user_id))
self._user_id = user_id
self.display_name = ('' if display_name is None else display_name)
@@ -138,7 +153,7 @@ class User(Model):
@dbconnection
def controls(self, store, email):
"""See `IUser`."""
- found = store.find(Address, email=email)
+ found = store.query(Address).filter_by(email=email)
if found.count() == 0:
return False
assert found.count() == 1, 'Unexpected count'
@@ -148,7 +163,7 @@ class User(Model):
def register(self, store, email, display_name=None):
"""See `IUser`."""
# First, see if the address already exists
- address = store.find(Address, email=email).one()
+ address = store.query(Address).filter_by(email=email).first()
if address is None:
if display_name is None:
display_name = ''
diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py
index 6f4a7ff5c..726aa6120 100644
--- a/src/mailman/model/usermanager.py
+++ b/src/mailman/model/usermanager.py
@@ -52,12 +52,12 @@ class UserManager:
@dbconnection
def delete_user(self, store, user):
"""See `IUserManager`."""
- store.remove(user)
+ store.delete(user)
@dbconnection
def get_user(self, store, email):
"""See `IUserManager`."""
- addresses = store.find(Address, email=email.lower())
+ addresses = store.query(Address).filter_by(email=email.lower())
if addresses.count() == 0:
return None
return addresses.one().user
@@ -65,7 +65,7 @@ class UserManager:
@dbconnection
def get_user_by_id(self, store, user_id):
"""See `IUserManager`."""
- users = store.find(User, _user_id=user_id)
+ users = store.query(User).filter_by(_user_id=user_id)
if users.count() == 0:
return None
return users.one()
@@ -74,13 +74,13 @@ class UserManager:
@dbconnection
def users(self, store):
"""See `IUserManager`."""
- for user in store.find(User):
+ for user in store.query(User).all():
yield user
@dbconnection
def create_address(self, store, email, display_name=None):
"""See `IUserManager`."""
- addresses = store.find(Address, email=email.lower())
+ addresses = store.query(Address).filter(Address.email==email.lower())
if addresses.count() == 1:
found = addresses[0]
raise ExistingAddressError(found.original_email)
@@ -101,12 +101,12 @@ class UserManager:
# unlinked before the address can be deleted.
if address.user:
address.user.unlink(address)
- store.remove(address)
+ store.delete(address)
@dbconnection
def get_address(self, store, email):
"""See `IUserManager`."""
- addresses = store.find(Address, email=email.lower())
+ addresses = store.query(Address).filter_by(email=email.lower())
if addresses.count() == 0:
return None
return addresses.one()
@@ -115,12 +115,12 @@ class UserManager:
@dbconnection
def addresses(self, store):
"""See `IUserManager`."""
- for address in store.find(Address):
+ for address in store.query(Address).all():
yield address
@property
@dbconnection
def members(self, store):
"""See `IUserManager."""
- for member in store.find(Member):
+ for member in store.query(Member).all():
yield member
diff --git a/src/mailman/model/version.py b/src/mailman/model/version.py
deleted file mode 100644
index e99fb0d1c..000000000
--- a/src/mailman/model/version.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (C) 2007-2014 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Model class for version numbers."""
-
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Version',
- ]
-
-from storm.locals import Int, Unicode
-from mailman.database.model import Model
-
-
-
-class Version(Model):
- id = Int(primary=True)
- component = Unicode()
- version = Unicode()
-
- # The testing machinery will generally reset all tables, however because
- # this table tracks schema migrations, we do not want to reset it.
- PRESERVE = True
-
- def __init__(self, component, version):
- super(Version, self).__init__()
- self.component = component
- self.version = version