summaryrefslogtreecommitdiff
path: root/Mailman/database
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/database')
-rw-r--r--Mailman/database/__init__.py50
-rw-r--r--Mailman/database/listmanager.py25
-rw-r--r--Mailman/database/messagestore.py31
-rw-r--r--Mailman/database/model/__init__.py47
-rw-r--r--Mailman/database/model/address.py37
-rw-r--r--Mailman/database/model/language.py8
-rw-r--r--Mailman/database/model/mailinglist.py233
-rw-r--r--Mailman/database/model/mailman.sql206
-rw-r--r--Mailman/database/model/member.py34
-rw-r--r--Mailman/database/model/message.py20
-rw-r--r--Mailman/database/model/pending.py85
-rw-r--r--Mailman/database/model/preferences.py27
-rw-r--r--Mailman/database/model/requests.py73
-rw-r--r--Mailman/database/model/roster.py48
-rw-r--r--Mailman/database/model/user.py30
-rw-r--r--Mailman/database/model/version.py15
-rw-r--r--Mailman/database/types.py54
-rw-r--r--Mailman/database/usermanager.py36
18 files changed, 685 insertions, 374 deletions
diff --git a/Mailman/database/__init__.py b/Mailman/database/__init__.py
index acc74642f..78b118be6 100644
--- a/Mailman/database/__init__.py
+++ b/Mailman/database/__init__.py
@@ -20,25 +20,18 @@ from __future__ import with_statement
__metaclass__ = type
__all__ = [
'StockDatabase',
- 'flush', # for test convenience
]
import os
from locknix.lockfile import Lock
-from elixir import objectstore
+from storm.properties import PropertyPublisherMeta
from zope.interface import implements
from Mailman.interfaces import IDatabase
from Mailman.database.listmanager import ListManager
from Mailman.database.usermanager import UserManager
from Mailman.database.messagestore import MessageStore
-from Mailman.database.model import Pendings
-from Mailman.database.model import Requests
-
-# Test suite convenience. Application code should use config.db.flush()
-# instead.
-flush = None
@@ -46,33 +39,54 @@ class StockDatabase:
implements(IDatabase)
def __init__(self):
- # Expose the flush() method for test case convenience using the stock
- # database.
- global flush
- flush = self.flush
self.list_manager = None
self.user_manager = None
self.message_store = None
self.pendings = None
self.requests = None
+ self._store = None
def initialize(self, debug=None):
+ # Avoid circular imports.
from Mailman.configuration import config
from Mailman.database import model
+ from Mailman.database.model import Pendings
+ from Mailman.database.model import Requests
# Serialize this so we don't get multiple processes trying to create
# the database at the same time.
with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')):
- model.initialize(debug)
+ self.store = model.initialize(debug)
self.list_manager = ListManager()
self.user_manager = UserManager()
self.message_store = MessageStore()
self.pendings = Pendings()
self.requests = Requests()
- self.flush()
-
- def flush(self):
- objectstore.flush()
def _reset(self):
- model._reset()
+ for model_class in _class_registry:
+ for row in self.store.find(model_class):
+ self.store.remove(row)
+
+
+
+_class_registry = set()
+
+
+class ModelMeta(PropertyPublisherMeta):
+ """Do more magic on table classes."""
+
+ def __init__(self, name, bases, dict):
+ # Before we let the base class do it's thing, force an __storm_table__
+ # property to enforce our table naming convention.
+ self.__storm_table__ = name.lower()
+ super(ModelMeta, self).__init__(name, bases, dict)
+ # Register the model class so that it can be more easily cleared.
+ # This is required by the test framework.
+ if name == 'Model':
+ return
+ _class_registry.add(self)
+
+class Model(object):
+ """Like Storm's `Storm` subclass, but with a bit extra."""
+ __metaclass__ = ModelMeta
diff --git a/Mailman/database/listmanager.py b/Mailman/database/listmanager.py
index 46f0aa859..48e7fe62c 100644
--- a/Mailman/database/listmanager.py
+++ b/Mailman/database/listmanager.py
@@ -15,17 +15,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""SQLAlchemy/Elixir based provider of IListManager."""
+"""A mailing list manager."""
import datetime
-from elixir import *
from zope.interface import implements
from Mailman import Errors
from Mailman.Utils import split_listname, fqdn_listname
from Mailman.configuration import config
-from Mailman.database.model import MailingList, Pendings
from Mailman.interfaces import IListManager
@@ -34,21 +32,30 @@ class ListManager(object):
implements(IListManager)
def create(self, fqdn_listname):
+ # Avoid circular imports.
+ from Mailman.database.model import MailingList
listname, hostname = split_listname(fqdn_listname)
- mlist = MailingList.get_by(list_name=listname,
- host_name=hostname)
+ mlist = config.db.store.find(
+ MailingList,
+ MailingList.list_name == listname,
+ MailingList.host_name == hostname).one()
if mlist:
raise Errors.MMListAlreadyExistsError(fqdn_listname)
mlist = MailingList(fqdn_listname)
mlist.created_at = datetime.datetime.now()
+ config.db.store.add(mlist)
return mlist
def delete(self, mlist):
- mlist.delete()
+ config.db.store.remove(mlist)
def get(self, fqdn_listname):
+ # Avoid circular imports.
+ from Mailman.database.model import MailingList
listname, hostname = split_listname(fqdn_listname)
- mlist = MailingList.get_by(list_name=listname, host_name=hostname)
+ mlist = config.db.store.find(MailingList,
+ list_name=listname,
+ host_name=hostname).one()
if mlist is not None:
# XXX Fixme
mlist._restore()
@@ -63,5 +70,7 @@ class ListManager(object):
@property
def names(self):
- for mlist in MailingList.query.filter_by().all():
+ # Avoid circular imports.
+ from Mailman.database.model import MailingList
+ for mlist in config.db.store.find(MailingList):
yield fqdn_listname(mlist.list_name, mlist.host_name)
diff --git a/Mailman/database/messagestore.py b/Mailman/database/messagestore.py
index e0e6cd9f1..7c90918ac 100644
--- a/Mailman/database/messagestore.py
+++ b/Mailman/database/messagestore.py
@@ -32,7 +32,6 @@ from zope.interface import implements
from Mailman import Utils
from Mailman.configuration import config
-from Mailman.database.model import Message
from Mailman.interfaces import IMessageStore
# It could be very bad if you have already stored files and you change this
@@ -46,6 +45,7 @@ class MessageStore:
implements(IMessageStore)
def add(self, message):
+ from Mailman.database.model import Message
# Ensure that the message has the requisite headers.
message_ids = message.get_all('message-id', [])
if len(message_ids) <> 1:
@@ -68,10 +68,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.
- result = Message.table.insert().execute(
- hash=hash32, path=relpath, message_id=message_id)
+ row = Message(hash=hash32, path=relpath, message_id=message_id)
# Add the additional header.
- seqno = result.last_inserted_ids()[0]
+ seqno = row.id
del message['X-List-Sequence-Number']
message['X-List-Sequence-Number'] = str(seqno)
# Now calculate the full file system path.
@@ -97,11 +96,21 @@ class MessageStore:
return pickle.load(fp)
def get_messages_by_message_id(self, message_id):
- for msgrow in Message.query.filter_by(message_id=message_id):
+ # Avoid circular imports.
+ from Mailman.database.model.message import Message
+ for msgrow in config.db.store.find(Message, message_id=message_id):
yield self._msgobj(msgrow)
def get_messages_by_hash(self, hash):
- for msgrow in Message.query.filter_by(hash=hash):
+ # Avoid circular imports.
+ from Mailman.database.model.message import Message
+ # 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 will
+ # always be an 8-string. Coerce to the latter if necessary; it must
+ # be US-ASCII.
+ if isinstance(hash, unicode):
+ hash = hash.encode('ascii')
+ for msgrow in config.db.store.find(Message, hash=hash):
yield self._msgobj(msgrow)
def _getmsg(self, global_id):
@@ -110,7 +119,9 @@ class MessageStore:
seqno = int(seqno)
except ValueError:
return None
- messages = Message.query.filter_by(id=seqno)
+ # Avoid circular imports.
+ from Mailman.database.model.message import Message
+ messages = config.db.store.find(Message, id=seqno)
if messages.count() == 0:
return None
assert messages.count() == 1, 'Multiple id matches'
@@ -126,11 +137,13 @@ class MessageStore:
@property
def messages(self):
- for msgrow in Message.query.filter_by().all():
+ # Avoid circular imports.
+ from Mailman.database.model.message import Message
+ for msgrow in config.db.store.find(Message):
yield self._msgobj(msgrow)
def delete_message(self, global_id):
msgrow = self._getmsg(global_id)
if msgrow is None:
raise KeyError(global_id)
- msgrow.delete()
+ config.db.store.remove(msgrow)
diff --git a/Mailman/database/model/__init__.py b/Mailman/database/model/__init__.py
index 86f79a84b..c15670403 100644
--- a/Mailman/database/model/__init__.py
+++ b/Mailman/database/model/__init__.py
@@ -15,6 +15,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
+from __future__ import with_statement
+
__all__ = [
'Address',
'Language',
@@ -28,9 +30,9 @@ __all__ = [
import os
import sys
-import elixir
-from sqlalchemy import create_engine
+from storm import database
+from storm.locals import create_database, Store
from string import Template
from urlparse import urlparse
@@ -39,12 +41,6 @@ import Mailman.Version
from Mailman import constants
from Mailman.Errors import SchemaVersionMismatchError
from Mailman.configuration import config
-
-# This /must/ be set before any Elixir classes are defined (i.e. imported).
-# This tells Elixir to use the short table names (i.e. the class name) instead
-# of a mangled full class path.
-elixir.options_defaults['shortnames'] = True
-
from Mailman.database.model.address import Address
from Mailman.database.model.language import Language
from Mailman.database.model.mailinglist import MailingList
@@ -59,8 +55,8 @@ from Mailman.database.model.version import Version
def initialize(debug):
- # Calculate the engine url
- url = Template(config.SQLALCHEMY_ENGINE_URL).safe_substitute(config.paths)
+ # Calculate the engine url.
+ url = Template(config.DEFAULT_DATABASE_URL).safe_substitute(config.paths)
# XXX By design of SQLite, database file creation does not honor
# umask. See their ticket #1193:
# http://www.sqlite.org/cvstrac/tktview?tn=1193,31
@@ -75,21 +71,30 @@ def initialize(debug):
# permissions. We only try to do this for SQLite engines, and yes, we
# could have chmod'd the file after the fact, but half dozen and all...
touch(url)
- engine = create_engine(url)
- engine.echo = (config.SQLALCHEMY_ECHO if debug is None else debug)
- elixir.metadata.bind = engine
- elixir.setup_all()
- elixir.create_all()
+ database = create_database(url)
+ store = Store(database)
+ database.DEBUG = (config.DEFAULT_DATABASE_ECHO if debug is None else debug)
+ # XXX Storm does not currently have schema creation. This is not an ideal
+ # way to handle creating the database, but it's cheap and easy for now.
+ import Mailman.database.model
+ schema_file = os.path.join(
+ os.path.dirname(Mailman.database.model.__file__),
+ 'mailman.sql')
+ with open(schema_file) as fp:
+ sql = fp.read()
+ for statement in sql.split(';'):
+ store.execute(statement + ';')
# Validate schema version.
- v = Version.get_by(component='schema')
+ v = store.find(Version, component=u'schema').one()
if not v:
# Database has not yet been initialized
- v = Version(component='schema',
+ v = Version(component=u'schema',
version=Mailman.Version.DATABASE_SCHEMA_VERSION)
- elixir.session.flush()
+ store.add(v)
elif v.version <> Mailman.Version.DATABASE_SCHEMA_VERSION:
# XXX Update schema
raise SchemaVersionMismatchError(v.version)
+ return store
def touch(url):
@@ -101,9 +106,3 @@ def touch(url):
# Ignore errors
if fd > 0:
os.close(fd)
-
-
-def _reset():
- for entity in elixir.entities:
- for row in entity.query.filter_by().all():
- row.delete()
diff --git a/Mailman/database/model/address.py b/Mailman/database/model/address.py
index 3ba3c3dbf..b8e2f0f31 100644
--- a/Mailman/database/model/address.py
+++ b/Mailman/database/model/address.py
@@ -15,31 +15,31 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
from email.utils import formataddr
+from storm.locals import *
from zope.interface import implements
from Mailman import Errors
+from Mailman.configuration import config
+from Mailman.database import Model
from Mailman.interfaces import IAddress
-MEMBER_KIND = 'Mailman.database.model.member.Member'
-PREFERENCE_KIND = 'Mailman.database.model.preferences.Preferences'
-USER_KIND = 'Mailman.database.model.user.User'
-
-
-class Address(Entity):
+class Address(Model):
implements(IAddress)
- address = Field(Unicode)
- _original = Field(Unicode)
- real_name = Field(Unicode)
- verified_on = Field(DateTime)
- registered_on = Field(DateTime)
+ id = Int(primary=True)
+ address = Unicode()
+ _original = Unicode()
+ real_name = Unicode()
+ verified_on = DateTime()
+ registered_on = DateTime()
- user = ManyToOne(USER_KIND)
- preferences = ManyToOne(PREFERENCE_KIND)
+ user_id = Int()
+ user = Reference(user_id, 'User.id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
def __init__(self, address, real_name):
super(Address, self).__init__()
@@ -66,9 +66,11 @@ class Address(Entity):
from Mailman.database.model import Member
from Mailman.database.model import Preferences
# This member has no preferences by default.
- member = Member.get_by(role=role,
- mailing_list=mailing_list.fqdn_listname,
- address=self)
+ member = config.db.store.find(
+ Member,
+ Member.role == role,
+ Member.mailing_list == mailing_list.fqdn_listname,
+ Member.address == self).one()
if member:
raise Errors.AlreadySubscribedError(
mailing_list.fqdn_listname, self.address, role)
@@ -76,6 +78,7 @@ class Address(Entity):
mailing_list=mailing_list.fqdn_listname,
address=self)
member.preferences = Preferences()
+ config.db.store.add(member)
return member
@property
diff --git a/Mailman/database/model/language.py b/Mailman/database/model/language.py
index ffdbd2cba..a5229ab6a 100644
--- a/Mailman/database/model/language.py
+++ b/Mailman/database/model/language.py
@@ -15,14 +15,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
+from storm.locals import *
from zope.interface import implements
+from Mailman.database import Model
from Mailman.interfaces import ILanguage
-class Language(Entity):
+class Language(Model):
implements(ILanguage)
- code = Field(Unicode)
+ id = Int(primary=True)
+ code = Unicode()
diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py
index 4057c2161..3a3396758 100644
--- a/Mailman/database/model/mailinglist.py
+++ b/Mailman/database/model/mailinglist.py
@@ -18,142 +18,145 @@
import os
import string
-from elixir import *
+from storm.locals import *
from zope.interface import implements
from Mailman.Utils import fqdn_listname, makedirs, split_listname
from Mailman.configuration import config
+from Mailman.database import Model
+from Mailman.database.types import Enum
from Mailman.interfaces import IMailingList, Personalization
-from Mailman.database.types import EnumType, TimeDeltaType
SPACE = ' '
UNDERSCORE = '_'
-class MailingList(Entity):
+class MailingList(Model):
implements(IMailingList)
+ id = Int(primary=True)
+
# List identity
- list_name = Field(Unicode)
- host_name = Field(Unicode)
+ list_name = Unicode()
+ host_name = Unicode()
# Attributes not directly modifiable via the web u/i
- created_at = Field(DateTime)
- web_page_url = Field(Unicode)
- admin_member_chunksize = Field(Integer)
- hold_and_cmd_autoresponses = Field(PickleType)
+ created_at = DateTime()
+ web_page_url = Unicode()
+ admin_member_chunksize = Int()
+ hold_and_cmd_autoresponses = Pickle()
# 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 = Field(Integer)
- next_digest_number = Field(Integer)
- admin_responses = Field(PickleType)
- postings_responses = Field(PickleType)
- request_responses = Field(PickleType)
- digest_last_sent_at = Field(Float)
- one_last_digest = Field(PickleType)
- volume = Field(Integer)
- last_post_time = Field(DateTime)
+ next_request_id = Int()
+ next_digest_number = Int()
+ admin_responses = Pickle()
+ postings_responses = Pickle()
+ request_responses = Pickle()
+ digest_last_sent_at = Float()
+ one_last_digest = Pickle()
+ volume = Int()
+ last_post_time = 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 = Field(PickleType)
- acceptable_aliases = Field(PickleType)
- admin_immed_notify = Field(Boolean)
- admin_notify_mchanges = Field(Boolean)
- administrivia = Field(Boolean)
- advertised = Field(Boolean)
- anonymous_list = Field(Boolean)
- archive = Field(Boolean)
- archive_private = Field(Boolean)
- archive_volume_frequency = Field(Integer)
- autorespond_admin = Field(Boolean)
- autorespond_postings = Field(Boolean)
- autorespond_requests = Field(Integer)
- autoresponse_admin_text = Field(Unicode)
- autoresponse_graceperiod = Field(TimeDeltaType)
- autoresponse_postings_text = Field(Unicode)
- autoresponse_request_text = Field(Unicode)
- ban_list = Field(PickleType)
- bounce_info_stale_after = Field(TimeDeltaType)
- bounce_matching_headers = Field(Unicode)
- bounce_notify_owner_on_disable = Field(Boolean)
- bounce_notify_owner_on_removal = Field(Boolean)
- bounce_processing = Field(Boolean)
- bounce_score_threshold = Field(Integer)
- bounce_unrecognized_goes_to_list_owner = Field(Boolean)
- bounce_you_are_disabled_warnings = Field(Integer)
- bounce_you_are_disabled_warnings_interval = Field(TimeDeltaType)
- collapse_alternatives = Field(Boolean)
- convert_html_to_plaintext = Field(Boolean)
- default_member_moderation = Field(Boolean)
- description = Field(Unicode)
- digest_footer = Field(Unicode)
- digest_header = Field(Unicode)
- digest_is_default = Field(Boolean)
- digest_send_periodic = Field(Boolean)
- digest_size_threshold = Field(Integer)
- digest_volume_frequency = Field(Integer)
- digestable = Field(Boolean)
- discard_these_nonmembers = Field(PickleType)
- emergency = Field(Boolean)
- encode_ascii_prefixes = Field(Boolean)
- filter_action = Field(Integer)
- filter_content = Field(Boolean)
- filter_filename_extensions = Field(PickleType)
- filter_mime_types = Field(PickleType)
- first_strip_reply_to = Field(Boolean)
- forward_auto_discards = Field(Boolean)
- gateway_to_mail = Field(Boolean)
- gateway_to_news = Field(Boolean)
- generic_nonmember_action = Field(Integer)
- goodbye_msg = Field(Unicode)
- header_filter_rules = Field(PickleType)
- hold_these_nonmembers = Field(PickleType)
- include_list_post_header = Field(Boolean)
- include_rfc2369_headers = Field(Boolean)
- info = Field(Unicode)
- linked_newsgroup = Field(Unicode)
- max_days_to_hold = Field(Integer)
- max_message_size = Field(Integer)
- max_num_recipients = Field(Integer)
- member_moderation_action = Field(Boolean)
- member_moderation_notice = Field(Unicode)
- mime_is_default_digest = Field(Boolean)
- moderator_password = Field(Unicode)
- msg_footer = Field(Unicode)
- msg_header = Field(Unicode)
- new_member_options = Field(Integer)
- news_moderation = Field(EnumType)
- news_prefix_subject_too = Field(Boolean)
- nntp_host = Field(Unicode)
- nondigestable = Field(Boolean)
- nonmember_rejection_notice = Field(Unicode)
- obscure_addresses = Field(Boolean)
- pass_filename_extensions = Field(PickleType)
- pass_mime_types = Field(PickleType)
- personalize = Field(EnumType)
- post_id = Field(Integer)
- preferred_language = Field(Unicode)
- private_roster = Field(Boolean)
- real_name = Field(Unicode)
- reject_these_nonmembers = Field(PickleType)
- reply_goes_to_list = Field(EnumType)
- reply_to_address = Field(Unicode)
- require_explicit_destination = Field(Boolean)
- respond_to_post_requests = Field(Boolean)
- scrub_nondigest = Field(Boolean)
- send_goodbye_msg = Field(Boolean)
- send_reminders = Field(Boolean)
- send_welcome_msg = Field(Boolean)
- subject_prefix = Field(Unicode)
- subscribe_auto_approval = Field(PickleType)
- subscribe_policy = Field(Integer)
- topics = Field(PickleType)
- topics_bodylines_limit = Field(Integer)
- topics_enabled = Field(Boolean)
- unsubscribe_policy = Field(Integer)
- welcome_msg = Field(Unicode)
+ accept_these_nonmembers = Pickle()
+ acceptable_aliases = Pickle()
+ admin_immed_notify = Bool()
+ admin_notify_mchanges = Bool()
+ administrivia = Bool()
+ advertised = Bool()
+ anonymous_list = Bool()
+ archive = Bool()
+ archive_private = Bool()
+ archive_volume_frequency = Int()
+ autorespond_admin = Bool()
+ autorespond_postings = Bool()
+ autorespond_requests = Int()
+ autoresponse_admin_text = Unicode()
+ autoresponse_graceperiod = TimeDelta()
+ autoresponse_postings_text = Unicode()
+ autoresponse_request_text = Unicode()
+ ban_list = Pickle()
+ bounce_info_stale_after = TimeDelta()
+ bounce_matching_headers = Unicode()
+ bounce_notify_owner_on_disable = Bool()
+ bounce_notify_owner_on_removal = Bool()
+ bounce_processing = Bool()
+ bounce_score_threshold = Int()
+ bounce_unrecognized_goes_to_list_owner = Bool()
+ bounce_you_are_disabled_warnings = Int()
+ bounce_you_are_disabled_warnings_interval = TimeDelta()
+ collapse_alternatives = Bool()
+ convert_html_to_plaintext = Bool()
+ default_member_moderation = Bool()
+ description = Unicode()
+ digest_footer = Unicode()
+ digest_header = Unicode()
+ digest_is_default = Bool()
+ digest_send_periodic = Bool()
+ digest_size_threshold = Int()
+ digest_volume_frequency = Int()
+ digestable = Bool()
+ discard_these_nonmembers = Pickle()
+ emergency = Bool()
+ encode_ascii_prefixes = Bool()
+ filter_action = Int()
+ filter_content = Bool()
+ filter_filename_extensions = Pickle()
+ filter_mime_types = Pickle()
+ first_strip_reply_to = Bool()
+ forward_auto_discards = Bool()
+ gateway_to_mail = Bool()
+ gateway_to_news = Bool()
+ generic_nonmember_action = Int()
+ goodbye_msg = Unicode()
+ header_filter_rules = Pickle()
+ hold_these_nonmembers = Pickle()
+ include_list_post_header = Bool()
+ include_rfc2369_headers = Bool()
+ info = Unicode()
+ linked_newsgroup = Unicode()
+ max_days_to_hold = Int()
+ max_message_size = Int()
+ max_num_recipients = Int()
+ member_moderation_action = Enum()
+ member_moderation_notice = Unicode()
+ mime_is_default_digest = Bool()
+ moderator_password = Unicode()
+ msg_footer = Unicode()
+ msg_header = Unicode()
+ new_member_options = Int()
+ news_moderation = Enum()
+ news_prefix_subject_too = Bool()
+ nntp_host = Unicode()
+ nondigestable = Bool()
+ nonmember_rejection_notice = Unicode()
+ obscure_addresses = Bool()
+ pass_filename_extensions = Pickle()
+ pass_mime_types = Pickle()
+ personalize = Enum()
+ post_id = Int()
+ preferred_language = Unicode()
+ private_roster = Bool()
+ real_name = Unicode()
+ reject_these_nonmembers = Pickle()
+ reply_goes_to_list = Enum()
+ reply_to_address = Unicode()
+ require_explicit_destination = Bool()
+ respond_to_post_requests = Bool()
+ scrub_nondigest = Bool()
+ send_goodbye_msg = Bool()
+ send_reminders = Bool()
+ send_welcome_msg = Bool()
+ subject_prefix = Unicode()
+ subscribe_auto_approval = Pickle()
+ subscribe_policy = Int()
+ topics = Pickle()
+ topics_bodylines_limit = Int()
+ topics_enabled = Bool()
+ unsubscribe_policy = Int()
+ welcome_msg = Unicode()
# Relationships
## has_and_belongs_to_many(
## 'available_languages',
diff --git a/Mailman/database/model/mailman.sql b/Mailman/database/model/mailman.sql
new file mode 100644
index 000000000..a20b1b118
--- /dev/null
+++ b/Mailman/database/model/mailman.sql
@@ -0,0 +1,206 @@
+CREATE TABLE _request (
+ id INTEGER NOT NULL,
+ "key" TEXT,
+ request_type TEXT,
+ data_hash TEXT,
+ mailing_list_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT _request_mailing_list_id_fk FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id)
+);
+CREATE TABLE address (
+ id INTEGER NOT NULL,
+ address TEXT,
+ _original TEXT,
+ real_name TEXT,
+ verified_on TIMESTAMP,
+ registered_on TIMESTAMP,
+ user_id INTEGER,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT address_user_id_fk FOREIGN KEY(user_id) REFERENCES user (id),
+ CONSTRAINT address_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE language (
+ id INTEGER NOT NULL,
+ code TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE mailinglist (
+ id INTEGER NOT NULL,
+ list_name TEXT,
+ host_name TEXT,
+ created_at TIMESTAMP,
+ web_page_url TEXT,
+ admin_member_chunksize INTEGER,
+ hold_and_cmd_autoresponses BLOB,
+ next_request_id INTEGER,
+ next_digest_number INTEGER,
+ admin_responses BLOB,
+ postings_responses BLOB,
+ request_responses BLOB,
+ digest_last_sent_at NUMERIC(10, 2),
+ one_last_digest BLOB,
+ volume INTEGER,
+ last_post_time TIMESTAMP,
+ accept_these_nonmembers BLOB,
+ acceptable_aliases BLOB,
+ admin_immed_notify BOOLEAN,
+ admin_notify_mchanges BOOLEAN,
+ administrivia BOOLEAN,
+ advertised BOOLEAN,
+ anonymous_list BOOLEAN,
+ archive BOOLEAN,
+ archive_private BOOLEAN,
+ archive_volume_frequency INTEGER,
+ autorespond_admin BOOLEAN,
+ autorespond_postings BOOLEAN,
+ autorespond_requests INTEGER,
+ autoresponse_admin_text TEXT,
+ autoresponse_graceperiod TEXT,
+ autoresponse_postings_text TEXT,
+ autoresponse_request_text TEXT,
+ ban_list BLOB,
+ bounce_info_stale_after TEXT,
+ bounce_matching_headers TEXT,
+ bounce_notify_owner_on_disable BOOLEAN,
+ bounce_notify_owner_on_removal BOOLEAN,
+ bounce_processing BOOLEAN,
+ bounce_score_threshold INTEGER,
+ bounce_unrecognized_goes_to_list_owner BOOLEAN,
+ bounce_you_are_disabled_warnings INTEGER,
+ bounce_you_are_disabled_warnings_interval TEXT,
+ collapse_alternatives BOOLEAN,
+ convert_html_to_plaintext BOOLEAN,
+ default_member_moderation BOOLEAN,
+ description TEXT,
+ digest_footer TEXT,
+ digest_header TEXT,
+ digest_is_default BOOLEAN,
+ digest_send_periodic BOOLEAN,
+ digest_size_threshold INTEGER,
+ digest_volume_frequency INTEGER,
+ digestable BOOLEAN,
+ discard_these_nonmembers BLOB,
+ emergency BOOLEAN,
+ encode_ascii_prefixes BOOLEAN,
+ filter_action INTEGER,
+ filter_content BOOLEAN,
+ filter_filename_extensions BLOB,
+ filter_mime_types BLOB,
+ first_strip_reply_to BOOLEAN,
+ forward_auto_discards BOOLEAN,
+ gateway_to_mail BOOLEAN,
+ gateway_to_news BOOLEAN,
+ generic_nonmember_action INTEGER,
+ goodbye_msg TEXT,
+ header_filter_rules BLOB,
+ hold_these_nonmembers BLOB,
+ include_list_post_header BOOLEAN,
+ include_rfc2369_headers BOOLEAN,
+ info TEXT,
+ linked_newsgroup TEXT,
+ max_days_to_hold INTEGER,
+ max_message_size INTEGER,
+ max_num_recipients INTEGER,
+ member_moderation_action BOOLEAN,
+ member_moderation_notice TEXT,
+ mime_is_default_digest BOOLEAN,
+ moderator_password TEXT,
+ msg_footer TEXT,
+ msg_header TEXT,
+ new_member_options INTEGER,
+ news_moderation TEXT,
+ news_prefix_subject_too BOOLEAN,
+ nntp_host TEXT,
+ nondigestable BOOLEAN,
+ nonmember_rejection_notice TEXT,
+ obscure_addresses BOOLEAN,
+ pass_filename_extensions BLOB,
+ pass_mime_types BLOB,
+ personalize TEXT,
+ post_id INTEGER,
+ preferred_language TEXT,
+ private_roster BOOLEAN,
+ real_name TEXT,
+ reject_these_nonmembers BLOB,
+ reply_goes_to_list TEXT,
+ reply_to_address TEXT,
+ require_explicit_destination BOOLEAN,
+ respond_to_post_requests BOOLEAN,
+ scrub_nondigest BOOLEAN,
+ send_goodbye_msg BOOLEAN,
+ send_reminders BOOLEAN,
+ send_welcome_msg BOOLEAN,
+ subject_prefix TEXT,
+ subscribe_auto_approval BLOB,
+ subscribe_policy INTEGER,
+ topics BLOB,
+ topics_bodylines_limit INTEGER,
+ topics_enabled BOOLEAN,
+ unsubscribe_policy INTEGER,
+ welcome_msg TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE member (
+ id INTEGER NOT NULL,
+ role TEXT,
+ mailing_list TEXT,
+ address_id INTEGER,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT member_address_id_fk FOREIGN KEY(address_id) REFERENCES address (id),
+ CONSTRAINT member_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE message (
+ id INTEGER NOT NULL,
+ hash TEXT,
+ path TEXT,
+ message_id TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE pended (
+ id INTEGER NOT NULL,
+ token TEXT,
+ expiration_date TIMESTAMP,
+ PRIMARY KEY (id)
+);
+CREATE TABLE pendedkeyvalue (
+ id INTEGER NOT NULL,
+ "key" TEXT,
+ value TEXT,
+ pended_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT pendedkeyvalue_pended_id_fk FOREIGN KEY(pended_id) REFERENCES pended (id)
+);
+CREATE TABLE preferences (
+ id INTEGER NOT NULL,
+ acknowledge_posts BOOLEAN,
+ hide_address BOOLEAN,
+ preferred_language TEXT,
+ receive_list_copy BOOLEAN,
+ receive_own_postings BOOLEAN,
+ delivery_mode TEXT,
+ delivery_status TEXT,
+ PRIMARY KEY (id)
+);
+CREATE TABLE user (
+ id INTEGER NOT NULL,
+ real_name TEXT,
+ password TEXT,
+ preferences_id INTEGER,
+ PRIMARY KEY (id),
+ CONSTRAINT user_preferences_id_fk FOREIGN KEY(preferences_id) REFERENCES preferences (id)
+);
+CREATE TABLE version (
+ id INTEGER NOT NULL,
+ component TEXT,
+ version INTEGER,
+ PRIMARY KEY (id)
+);
+CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id);
+CREATE INDEX ix_address_preferences_id ON address (preferences_id);
+CREATE INDEX ix_address_user_id ON address (user_id);
+CREATE INDEX ix_member_address_id ON member (address_id);
+CREATE INDEX ix_member_preferences_id ON member (preferences_id);
+CREATE INDEX ix_pendedkeyvalue_pended_id ON pendedkeyvalue (pended_id);
+CREATE INDEX ix_user_preferences_id ON user (preferences_id);
diff --git a/Mailman/database/model/member.py b/Mailman/database/model/member.py
index 4f353a06c..3f9775d3c 100644
--- a/Mailman/database/model/member.py
+++ b/Mailman/database/model/member.py
@@ -15,28 +15,34 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
+from storm.locals import *
from zope.interface import implements
from Mailman.Utils import split_listname
+from Mailman.configuration import config
from Mailman.constants import SystemDefaultPreferences
-from Mailman.database.types import EnumType
+from Mailman.database import Model
+from Mailman.database.types import Enum
from Mailman.interfaces import IMember, IPreferences
-ADDRESS_KIND = 'Mailman.database.model.address.Address'
-PREFERENCE_KIND = 'Mailman.database.model.preferences.Preferences'
-
-
-class Member(Entity):
+class Member(Model):
implements(IMember)
- role = Field(EnumType)
- mailing_list = Field(Unicode)
- # Relationships
- address = ManyToOne(ADDRESS_KIND)
- preferences = ManyToOne(PREFERENCE_KIND)
+ id = Int(primary=True)
+ role = Enum()
+ mailing_list = Unicode()
+
+ address_id = Int()
+ address = Reference(address_id, 'Address.id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
+
+ def __init__(self, role, mailing_list, address):
+ self.role = role
+ self.mailing_list = mailing_list
+ self.address = address
def __repr__(self):
return '<Member: %s on %s as %s>' % (
@@ -85,5 +91,5 @@ class Member(Entity):
return 'http://example.com/' + self.address.address
def unsubscribe(self):
- self.preferences.delete()
- self.delete()
+ config.db.store.remove(self.preferences)
+ config.db.store.remove(self)
diff --git a/Mailman/database/model/message.py b/Mailman/database/model/message.py
index eb4b4616d..c4ea4a636 100644
--- a/Mailman/database/model/message.py
+++ b/Mailman/database/model/message.py
@@ -15,18 +15,28 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
+from storm.locals import *
from zope.interface import implements
+from Mailman.configuration import config
+from Mailman.database import Model
from Mailman.interfaces import IMessage
-class Message(Entity):
+class Message(Model):
"""A message in the message store."""
implements(IMessage)
- hash = Field(Unicode)
- path = Field(Unicode)
- message_id = Field(Unicode)
+ id = Int(primary=True, default=AutoReload)
+ message_id = Unicode()
+ hash = RawStr()
+ path = RawStr()
+ # This is a Messge-ID field representation, not a database row id.
+
+ def __init__(self, message_id, hash, path):
+ self.message_id = message_id
+ self.hash = hash
+ self.path = path
+ config.db.store.add(self)
diff --git a/Mailman/database/model/pending.py b/Mailman/database/model/pending.py
index 75bb59d3c..3f3e2caa0 100644
--- a/Mailman/database/model/pending.py
+++ b/Mailman/database/model/pending.py
@@ -17,40 +17,51 @@
"""Implementations of the IPendable and IPending interfaces."""
+import sys
import time
import random
import hashlib
import datetime
-from elixir import *
+from storm.locals import *
from zope.interface import implements
from zope.interface.verify import verifyObject
from Mailman.configuration import config
+from Mailman.database import Model
from Mailman.interfaces import (
- IPendings, IPendable, IPendedKeyValue, IPended)
-
-PEND_KIND = 'Mailman.database.model.pending.Pended'
+ IPendable, IPended, IPendedKeyValue, IPendings)
-class PendedKeyValue(Entity):
+class PendedKeyValue(Model):
"""A pended key/value pair, tied to a token."""
implements(IPendedKeyValue)
- key = Field(Unicode)
- value = Field(Unicode)
- pended = ManyToOne(PEND_KIND)
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+
+ id = Int(primary=True)
+ key = Unicode()
+ value = Unicode()
+ pended_id = Int()
-class Pended(Entity):
+class Pended(Model):
"""A pended event, tied to a token."""
implements(IPended)
- token = Field(Unicode)
- expiration_date = Field(DateTime)
+ def __init__(self, token, expiration_date):
+ self.token = token
+ self.expiration_date = expiration_date
+
+ id = Int(primary=True)
+ token = RawStr()
+ expiration_date = DateTime()
+ key_values = ReferenceSet(id, PendedKeyValue.pended_id)
@@ -82,7 +93,7 @@ class Pendings(object):
token = hashlib.sha1(repr(x)).hexdigest()
# In practice, we'll never get a duplicate, but we'll be anal
# about checking anyway.
- if Pended.query.filter_by(token=token).count() == 0:
+ if config.db.store.find(Pended, token=token).count() == 0:
break
else:
raise AssertionError('Could not find a valid pendings token')
@@ -91,11 +102,24 @@ class Pendings(object):
token=token,
expiration_date=datetime.datetime.now() + lifetime)
for key, value in pendable.items():
- PendedKeyValue(key=key, value=value, pended=pending)
+ if isinstance(key, str):
+ key = unicode(key, 'utf-8')
+ if isinstance(value, str):
+ value = unicode(value, 'utf-8')
+ elif type(value) is int:
+ value = u'__builtin__.int\1%s' % value
+ elif type(value) is float:
+ value = u'__builtin__.float\1%s' % value
+ elif type(value) is bool:
+ value = u'__builtin__.bool\1%s' % value
+ keyval = PendedKeyValue(key=key, value=value)
+ pending.key_values.add(keyval)
+ config.db.store.add(pending)
return token
def confirm(self, token, expunge=True):
- pendings = Pended.query.filter_by(token=token)
+ store = config.db.store
+ pendings = store.find(Pended, token=token)
if pendings.count() == 0:
return None
assert pendings.count() == 1, (
@@ -103,27 +127,32 @@ class Pendings(object):
pending = pendings[0]
pendable = UnpendedPendable()
# Find all PendedKeyValue entries that are associated with the pending
- # object's ID.
- q = PendedKeyValue.query.filter(
- PendedKeyValue.c.pended_id == Pended.c.id).filter(
- Pended.c.id == pending.id)
- for keyvalue in q.all():
- pendable[keyvalue.key] = keyvalue.value
+ # object's ID. Watch out for type conversions.
+ for keyvalue in store.find(PendedKeyValue,
+ PendedKeyValue.pended_id == pending.id):
+ if keyvalue.value is not None and '\1' in keyvalue.value:
+ typename, value = keyvalue.value.split('\1', 1)
+ package, classname = typename.rsplit('.', 1)
+ __import__(package)
+ module = sys.modules[package]
+ pendable[keyvalue.key] = getattr(module, classname)(value)
+ else:
+ pendable[keyvalue.key] = keyvalue.value
if expunge:
- keyvalue.delete()
+ store.remove(keyvalue)
if expunge:
- pending.delete()
+ store.remove(pending)
return pendable
def evict(self):
+ store = config.db.store
now = datetime.datetime.now()
- for pending in Pended.query.filter_by().all():
+ for pending in store.find(Pended):
if pending.expiration_date < now:
# Find all PendedKeyValue entries that are associated with the
# pending object's ID.
- q = PendedKeyValue.query.filter(
- PendedKeyValue.c.pended_id == Pended.c.id).filter(
- Pended.c.id == pending.id)
+ q = store.find(PendedKeyValue,
+ PendedKeyValue.pended_id == pending.id)
for keyvalue in q:
- keyvalue.delete()
- pending.delete()
+ store.remove(keyvalue)
+ store.remove(pending)
diff --git a/Mailman/database/model/preferences.py b/Mailman/database/model/preferences.py
index 8cbb77e6a..65d909bd0 100644
--- a/Mailman/database/model/preferences.py
+++ b/Mailman/database/model/preferences.py
@@ -15,29 +15,26 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
-from email.utils import formataddr
+from storm.locals import *
from zope.interface import implements
-from Mailman.database.types import EnumType
+from Mailman.database import Model
+from Mailman.database.types import Enum
from Mailman.interfaces import IPreferences
-ADDRESS_KIND = 'Mailman.database.model.address.Address'
-MEMBER_KIND = 'Mailman.database.model.member.Member'
-USER_KIND = 'Mailman.database.model.user.User'
-
-class Preferences(Entity):
+class Preferences(Model):
implements(IPreferences)
- acknowledge_posts = Field(Boolean)
- hide_address = Field(Boolean)
- preferred_language = Field(Unicode)
- receive_list_copy = Field(Boolean)
- receive_own_postings = Field(Boolean)
- delivery_mode = Field(EnumType)
- delivery_status = Field(EnumType)
+ id = Int(primary=True)
+ acknowledge_posts = Bool()
+ hide_address = Bool()
+ preferred_language = Unicode()
+ receive_list_copy = Bool()
+ receive_own_postings = Bool()
+ delivery_mode = Enum()
+ delivery_status = Enum()
def __repr__(self):
return '<Preferences object at %#x>' % id(self)
diff --git a/Mailman/database/model/requests.py b/Mailman/database/model/requests.py
index 037483c1a..64fff7c48 100644
--- a/Mailman/database/model/requests.py
+++ b/Mailman/database/model/requests.py
@@ -18,17 +18,15 @@
"""Implementations of the IRequests and IListRequests interfaces."""
from datetime import timedelta
-from elixir import *
+from storm.locals import *
from zope.interface import implements
from Mailman.configuration import config
-from Mailman.database.types import EnumType
+from Mailman.database import Model
+from Mailman.database.types import Enum
from Mailman.interfaces import IListRequests, IPendable, IRequests, RequestType
-MAILINGLIST_KIND = 'Mailman.database.model.mailinglist.MailingList'
-
-
__metaclass__ = type
__all__ = [
'Requests',
@@ -49,21 +47,25 @@ class ListRequests:
@property
def count(self):
- return _Request.query.filter_by(mailing_list=self.mailing_list).count()
+ return config.db.store.find(
+ _Request, mailing_list=self.mailing_list).count()
def count_of(self, request_type):
- return _Request.query.filter_by(mailing_list=self.mailing_list,
- type=request_type).count()
+ return config.db.store.find(
+ _Request,
+ mailing_list=self.mailing_list, request_type=request_type).count()
@property
def held_requests(self):
- results = _Request.query.filter_by(mailing_list=self.mailing_list)
+ results = config.db.store.find(
+ _Request, mailing_list=self.mailing_list)
for request in results:
yield request
def of_type(self, request_type):
- results = _Request.query.filter_by(mailing_list=self.mailing_list,
- type=request_type)
+ results = config.db.store.find(
+ _Request,
+ mailing_list=self.mailing_list, request_type=request_type)
for request in results:
yield request
@@ -81,25 +83,12 @@ class ListRequests:
pendable.update(data)
token = config.db.pendings.add(pendable, timedelta(days=5000))
data_hash = token
- # XXX This would be a good other way to do it, but it causes the
- # select_by()'s in .count and .held_requests() to fail, even with
- # flush()'s.
-## result = _Request.table.insert().execute(
-## key=key, type=request_type,
-## mailing_list=self.mailing_list,
-## data_hash=data_hash)
-## row_id = result.last_inserted_ids()[0]
-## return row_id
- result = _Request(key=key, type=request_type,
- mailing_list=self.mailing_list,
- data_hash=data_hash)
- # XXX We need a handle on last_inserted_ids() instead of requiring a
- # flush of the database to get a valid id.
- config.db.flush()
- return result.id
+ request = _Request(key, request_type, self.mailing_list, data_hash)
+ config.db.store.add(request)
+ return request.id
def get_request(self, request_id):
- result = _Request.get(request_id)
+ result = config.db.store.get(_Request, request_id)
if result is None:
return None
if result.data_hash is None:
@@ -110,12 +99,12 @@ class ListRequests:
return result.key, data
def delete_request(self, request_id):
- result = _Request.get(request_id)
- if result is None:
+ request = config.db.store.get(_Request, request_id)
+ if request is None:
raise KeyError(request_id)
# Throw away the pended data.
- config.db.pendings.confirm(result.data_hash)
- result.delete()
+ config.db.pendings.confirm(request.data_hash)
+ config.db.store.remove(request)
@@ -127,11 +116,19 @@ class Requests:
-class _Request(Entity):
+class _Request(Model):
"""Table for mailing list hold requests."""
- key = Field(Unicode)
- type = Field(EnumType)
- data_hash = Field(Unicode)
- # Relationships
- mailing_list = ManyToOne(MAILINGLIST_KIND)
+ id = Int(primary=True, default=AutoReload)
+ key = Unicode()
+ request_type = Enum()
+ data_hash = RawStr()
+
+ mailing_list_id = Int()
+ mailing_list = Reference(mailing_list_id, 'MailingList.id')
+
+ def __init__(self, key, request_type, mailing_list, data_hash):
+ self.key = key
+ self.request_type = request_type
+ self.mailing_list = mailing_list
+ self.data_hash = data_hash
diff --git a/Mailman/database/model/roster.py b/Mailman/database/model/roster.py
index c8fa86d58..55723893d 100644
--- a/Mailman/database/model/roster.py
+++ b/Mailman/database/model/roster.py
@@ -22,9 +22,10 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
-from sqlalchemy import *
+from storm.locals import *
from zope.interface import implements
+from Mailman.configuration import config
from Mailman.constants import SystemDefaultPreferences
from Mailman.database.model import Address, Member
from Mailman.interfaces import DeliveryMode, IRoster, MemberRole
@@ -49,7 +50,8 @@ class AbstractRoster(object):
@property
def members(self):
- for member in Member.query.filter_by(
+ for member in config.db.store.find(
+ Member,
mailing_list=self._mlist.fqdn_listname,
role=self.role):
yield member
@@ -73,11 +75,12 @@ class AbstractRoster(object):
yield member.address
def get_member(self, address):
- results = Member.query.filter(
- and_(Member.c.mailing_list == self._mlist.fqdn_listname,
- Member.c.role == self.role,
- Address.c.address == address,
- Member.c.address_id == Address.c.id))
+ results = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Member.role == self.role,
+ Address.address == address,
+ Member.address_id == Address.id)
if results.count() == 0:
return None
elif results.count() == 1:
@@ -121,20 +124,22 @@ class AdministratorRoster(AbstractRoster):
def members(self):
# Administrators are defined as the union of the owners and the
# moderators.
- members = Member.query.filter(
- and_(Member.c.mailing_list == self._mlist.fqdn_listname,
- or_(Member.c.role == MemberRole.owner,
- Member.c.role == MemberRole.moderator)))
+ members = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Or(Member.role == MemberRole.owner,
+ Member.role == MemberRole.moderator))
for member in members:
yield member
def get_member(self, address):
- results = Member.query.filter(
- and_(Member.c.mailing_list == self._mlist.fqdn_listname,
- or_(Member.c.role == MemberRole.moderator,
- Member.c.role == MemberRole.owner),
- Address.c.address == address,
- Member.c.address_id == Address.c.id))
+ results = config.db.store.find(
+ Member,
+ Member.mailing_list == self._mlist.fqdn_listname,
+ Or(Member.role == MemberRole.moderator,
+ Member.role == MemberRole.owner),
+ Address.address == address,
+ Member.address_id == Address.id)
if results.count() == 0:
return None
elif results.count() == 1:
@@ -155,7 +160,8 @@ class RegularMemberRoster(AbstractRoster):
# Query for all the Members which have a role of MemberRole.member and
# are subscribed to this mailing list. Then return only those members
# that have a regular delivery mode.
- for member in Member.query.filter_by(
+ for member in config.db.store.find(
+ Member,
mailing_list=self._mlist.fqdn_listname,
role=MemberRole.member):
if member.delivery_mode == DeliveryMode.regular:
@@ -181,7 +187,8 @@ class DigestMemberRoster(AbstractRoster):
# Query for all the Members which have a role of MemberRole.member and
# are subscribed to this mailing list. Then return only those members
# that have one of the digest delivery modes.
- for member in Member.query.filter_by(
+ for member in config.db.store.find(
+ Member,
mailing_list=self._mlist.fqdn_listname,
role=MemberRole.member):
if member.delivery_mode in _digest_modes:
@@ -196,6 +203,7 @@ class Subscribers(AbstractRoster):
@property
def members(self):
- for member in Member.query.filter_by(
+ for member in config.db.store.find(
+ Member,
mailing_list=self._mlist.fqdn_listname):
yield member
diff --git a/Mailman/database/model/user.py b/Mailman/database/model/user.py
index 17c388e0e..84b5a4595 100644
--- a/Mailman/database/model/user.py
+++ b/Mailman/database/model/user.py
@@ -15,28 +15,29 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
from email.utils import formataddr
+from storm.locals import *
from zope.interface import implements
from Mailman import Errors
+from Mailman.configuration import config
+from Mailman.database import Model
from Mailman.database.model import Address
from Mailman.database.model import Preferences
from Mailman.interfaces import IUser
-ADDRESS_KIND = 'Mailman.database.model.address.Address'
-PREFERENCE_KIND = 'Mailman.database.model.preferences.Preferences'
-
-class User(Entity):
+class User(Model):
implements(IUser)
- real_name = Field(Unicode)
- password = Field(Unicode)
+ id = Int(primary=True)
+ real_name = Unicode()
+ password = Unicode()
- addresses = OneToMany(ADDRESS_KIND)
- preferences = ManyToOne(PREFERENCE_KIND)
+ addresses = ReferenceSet(id, 'Address.user_id')
+ preferences_id = Int()
+ preferences = Reference(preferences_id, 'Preferences.id')
def __repr__(self):
return '<User "%s" at %#x>' % (self.real_name, id(self))
@@ -52,15 +53,18 @@ class User(Entity):
address.user = None
def controls(self, address):
- found = Address.get_by(address=address)
- return bool(found and found.user is self)
+ found = config.db.store.find(Address, address=address)
+ if found.count() == 0:
+ return False
+ assert found.count() == 1, 'Unexpected count'
+ return found[0].user is self
def register(self, address, real_name=None):
# First, see if the address already exists
- addrobj = Address.get_by(address=address)
+ addrobj = config.db.store.find(Address, address=address).one()
if addrobj is None:
if real_name is None:
- real_name = ''
+ real_name = u''
addrobj = Address(address=address, real_name=real_name)
addrobj.preferences = Preferences()
# Link the address to the user if it is not already linked.
diff --git a/Mailman/database/model/version.py b/Mailman/database/model/version.py
index dbbf5b8c1..2f4ff3f4d 100644
--- a/Mailman/database/model/version.py
+++ b/Mailman/database/model/version.py
@@ -15,9 +15,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-from elixir import *
+from storm.locals import *
+from Mailman.database import Model
-class Version(Entity):
- component = Field(Unicode)
- version = Field(Integer)
+
+class Version(Model):
+ id = Int(primary=True)
+ component = Unicode()
+ version = Int()
+
+ def __init__(self, component, version):
+ self.component = component
+ self.version = version
diff --git a/Mailman/database/types.py b/Mailman/database/types.py
index 0f0e46fa3..ec886699d 100644
--- a/Mailman/database/types.py
+++ b/Mailman/database/types.py
@@ -15,51 +15,43 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
+__all__ = [
+ 'Enum',
+ ]
+
+
import sys
-from datetime import timedelta
-from sqlalchemy import types
+from storm.properties import SimpleProperty
+from storm.variables import UnicodeVariable, Variable
-# SQLAlchemy custom type for storing munepy Enums in the database.
-class EnumType(types.TypeDecorator):
- # Enums can be stored as strings of the form:
- # full.path.to.Enum:intval
- impl = types.String
+class _EnumVariable(Variable):
+ """Storm variable."""
- def convert_bind_param(self, value, engine):
- if value is None:
- return None
- return '%s.%s:%d' % (value.enumclass.__module__,
- value.enumclass.__name__,
- int(value))
-
- def convert_result_value(self, value, engine):
+ def parse_set(self, value, from_db):
if value is None:
return None
+ if not from_db:
+ return value
path, intvalue = value.rsplit(':', 1)
modulename, classname = path.rsplit('.', 1)
__import__(modulename)
cls = getattr(sys.modules[modulename], classname)
return cls[int(intvalue)]
-
-
-class TimeDeltaType(types.TypeDecorator):
- # timedeltas are stored as the string representation of three integers,
- # separated by colons. The values represent the three timedelta
- # attributes days, seconds, microseconds.
- impl = types.String
-
- def convert_bind_param(self, value, engine):
+ def parse_get(self, value, to_db):
if value is None:
return None
- return '%s:%s:%s' % (value.days, value.seconds, value.microseconds)
+ if not to_db:
+ return value
+ return '%s.%s:%d' % (value.enumclass.__module__,
+ value.enumclass.__name__,
+ int(value))
- def convert_result_value(self, value, engine):
- if value is None:
- return None
- parts = value.split(':')
- assert len(parts) == 3, 'Bad timedelta representation: %s' % value
- return timedelta(*(int(value) for value in parts))
+
+class Enum(SimpleProperty):
+ """Custom munepy.Enum type for Storm."""
+
+ variable_class = _EnumVariable
diff --git a/Mailman/database/usermanager.py b/Mailman/database/usermanager.py
index 6bc2ed53d..ab41409d8 100644
--- a/Mailman/database/usermanager.py
+++ b/Mailman/database/usermanager.py
@@ -15,18 +15,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""SQLAlchemy/Elixir based provider of IUserManager."""
+"""A user manager."""
from __future__ import with_statement
import os
-from elixir import *
from zope.interface import implements
from Mailman import Errors
from Mailman.configuration import config
-from Mailman.database.model import *
from Mailman.interfaces import IUserManager
@@ -35,25 +33,32 @@ class UserManager(object):
implements(IUserManager)
def create_user(self, address=None, real_name=None):
+ # Avoid circular imports.
+ from Mailman.database.model import Address, Preferences, User
user = User()
- user.real_name = ('' if real_name is None else real_name)
+ user.real_name = (u'' if real_name is None else real_name)
if address:
addrobj = Address(address, user.real_name)
addrobj.preferences = Preferences()
user.link(addrobj)
user.preferences = Preferences()
+ config.db.store.add(user)
return user
def delete_user(self, user):
- user.delete()
+ config.db.store.remove(user)
@property
def users(self):
- for user in User.query.filter_by().all():
+ # Avoid circular imports.
+ from Mailman.database.model import User
+ for user in config.db.store.find(User):
yield user
def get_user(self, address):
- addresses = Address.query.filter_by(address=address.lower())
+ # Avoid circular imports.
+ from Mailman.database.model import Address
+ addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 0:
return None
elif addresses.count() == 1:
@@ -62,17 +67,20 @@ class UserManager(object):
raise AssertionError('Unexpected query count')
def create_address(self, address, real_name=None):
- addresses = Address.query.filter_by(address=address.lower())
+ # Avoid circular imports.
+ from Mailman.database.model import Address, Preferences
+ addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 1:
found = addresses[0]
raise Errors.ExistingAddressError(found.original_address)
assert addresses.count() == 0, 'Unexpected results'
if real_name is None:
- real_name = ''
+ real_name = u''
# It's okay not to lower case the 'address' argument because the
# constructor will do the right thing.
address = Address(address, real_name)
address.preferences = Preferences()
+ config.db.store.add(address)
return address
def delete_address(self, address):
@@ -80,10 +88,12 @@ class UserManager(object):
# unlinked before the address can be deleted.
if address.user:
address.user.unlink(address)
- address.delete()
+ config.db.store.remove(address)
def get_address(self, address):
- addresses = Address.query.filter_by(address=address.lower())
+ # Avoid circular imports.
+ from Mailman.database.model import Address
+ addresses = config.db.store.find(Address, address=address.lower())
if addresses.count() == 0:
return None
elif addresses.count() == 1:
@@ -93,5 +103,7 @@ class UserManager(object):
@property
def addresses(self):
- for address in Address.query.filter_by().all():
+ # Avoid circular imports.
+ from Mailman.database.model.address import Address
+ for address in config.db.store.find(Address):
yield address