summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/subscriptions.py13
-rw-r--r--src/mailman/database/base.py104
-rw-r--r--src/mailman/database/factory.py4
-rw-r--r--src/mailman/database/model.py46
-rw-r--r--src/mailman/database/postgresql.py4
-rw-r--r--src/mailman/database/sqlite.py6
-rw-r--r--src/mailman/database/types.py107
-rw-r--r--src/mailman/interfaces/database.py2
-rw-r--r--src/mailman/model/address.py28
-rw-r--r--src/mailman/model/autorespond.py43
-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/domain.py27
-rw-r--r--src/mailman/model/language.py8
-rw-r--r--src/mailman/model/listmanager.py22
-rw-r--r--src/mailman/model/mailinglist.py369
-rw-r--r--src/mailman/model/member.py34
-rw-r--r--src/mailman/model/message.py13
-rw-r--r--src/mailman/model/messagestore.py12
-rw-r--r--src/mailman/model/mime.py15
-rw-r--r--src/mailman/model/pending.py46
-rw-r--r--src/mailman/model/preferences.py20
-rw-r--r--src/mailman/model/requests.py35
-rw-r--r--src/mailman/model/roster.py41
-rw-r--r--src/mailman/model/tests/test_listmanager.py8
-rw-r--r--src/mailman/model/uid.py15
-rw-r--r--src/mailman/model/user.py47
-rw-r--r--src/mailman/model/usermanager.py18
-rw-r--r--src/mailman/model/version.py13
-rw-r--r--src/mailman/utilities/importer.py20
31 files changed, 612 insertions, 577 deletions
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index b2560beb5..303303e70 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -28,7 +28,7 @@ __all__ = [
from operator import attrgetter
from passlib.utils import generate_password as generate
-from storm.expr import And, Or
+from sqlalchemy import and_, or_
from uuid import UUID
from zope.component import getUtility
from zope.interface import implementer
@@ -88,8 +88,7 @@ class SubscriptionService:
@dbconnection
def get_member(self, store, member_id):
"""See `ISubscriptionService`."""
- members = store.find(
- Member,
+ members = store.query(Member).filter(
Member._member_id == member_id)
if members.count() == 0:
return None
@@ -117,7 +116,7 @@ class SubscriptionService:
# This probably could be made more efficient.
if address is None or user is None:
return []
- query.append(Or(Member.address_id == address.id,
+ query.append(or_(Member.address_id == address.id,
Member.user_id == user.id))
else:
# subscriber is a user id.
@@ -126,15 +125,15 @@ class SubscriptionService:
if address.id is not None)
if len(address_ids) == 0 or user is None:
return []
- query.append(Or(Member.user_id == user.id,
- Member.address_id.is_in(address_ids)))
+ query.append(or_(Member.user_id == user.id,
+ Member.address_id.in_(address_ids)))
# Calculate the rest of the query expression, which will get And'd
# with the Or clause above (if there is one).
if list_id is not None:
query.append(Member.list_id == list_id)
if role is not None:
query.append(Member.role == role)
- results = store.find(Member, And(*query))
+ results = store.query(Member).filter(and_(*query))
return sorted(results, key=_membership_sort_key)
def __iter__(self):
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index cbf88a4ff..f379b3124 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -29,8 +29,9 @@ import logging
from lazr.config import as_boolean
from pkg_resources import resource_listdir, resource_string
-from storm.cache import GenerationalCache
-from storm.locals import create_database, Store
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm.session import Session
from zope.interface import implementer
from mailman.config import config
@@ -45,23 +46,24 @@ NL = '\n'
@implementer(IDatabase)
-class StormBaseDatabase:
- """The database base class for use with the Storm ORM.
+class SABaseDatabase:
+ """The database base class for use with SQLAlchemy.
- Use this as a base class for your DB-specific derived classes.
+ Use this as a base class for your DB-Specific derived classes.
"""
-
# Tag used to distinguish the database being used. Override this in base
# classes.
+
TAG = ''
def __init__(self):
self.url = None
self.store = None
+ self.transaction = None
def begin(self):
"""See `IDatabase`."""
- # Storm takes care of this for us.
+ # SA does this for us.
pass
def commit(self):
@@ -100,18 +102,9 @@ class StormBaseDatabase:
"""
pass
- def _prepare(self, url):
- """Prepare the database for creation.
-
- Some database backends need to do so me prep work before letting Storm
- create the database. For example, we have to touch the SQLite .db
- file first so that it has the proper file modes.
- """
- pass
-
def initialize(self, debug=None):
- """See `IDatabase`."""
- # Calculate the engine url.
+ """See `IDatabase`"""
+ # Calculate the engine url
url = expand(config.database.url, config.paths)
log.debug('Database url: %s', url)
# XXX By design of SQLite, database file creation does not honor
@@ -129,13 +122,10 @@ class StormBaseDatabase:
# engines, and yes, we could have chmod'd the file after the fact, but
# half dozen and all...
self.url = url
- self._prepare(url)
- database = create_database(url)
- store = Store(database, GenerationalCache())
- database.DEBUG = (as_boolean(config.database.debug)
- if debug is None else debug)
- self.store = store
- store.commit()
+ self.engine = create_engine(url)
+ session = sessionmaker(bind=self.engine)
+ self.store = session()
+ self.store.commit()
def load_migrations(self, until=None):
"""Load schema migrations.
@@ -144,45 +134,8 @@ class StormBaseDatabase:
With default value of None, load all migrations.
:type until: string
"""
- migrations_path = config.database.migrations_path
- if '.' in migrations_path:
- parent, dot, child = migrations_path.rpartition('.')
- else:
- parent = migrations_path
- child = ''
- # If the database does not yet exist, load the base schema.
- filenames = sorted(resource_listdir(parent, child))
- # Find out which schema migrations have already been loaded.
- if self._database_exists(self.store):
- versions = set(version.version for version in
- self.store.find(Version, component='schema'))
- else:
- versions = set()
- for filename in filenames:
- module_fn, extension = os.path.splitext(filename)
- if extension != '.py':
- continue
- parts = module_fn.split('_')
- if len(parts) < 2:
- continue
- version = parts[1].strip()
- if len(version) == 0:
- # Not a schema migration file.
- continue
- if version in versions:
- log.debug('already migrated to %s', version)
- continue
- if until is not None and version > until:
- # We're done.
- break
- module_path = migrations_path + '.' + module_fn
- __import__(module_path)
- upgrade = getattr(sys.modules[module_path], 'upgrade', None)
- if upgrade is None:
- continue
- log.debug('migrating db to %s: %s', version, module_path)
- upgrade(self, self.store, version, module_path)
- self.commit()
+ from mailman.database.model import Model
+ Model.metadata.create_all(self.engine)
def load_sql(self, store, sql):
"""Load the given SQL into the store.
@@ -200,29 +153,6 @@ class StormBaseDatabase:
if statement.strip() != '':
store.execute(statement + ';')
- def load_schema(self, store, version, filename, module_path):
- """Load the schema from a file.
-
- This is a helper method for migration classes to call.
-
- :param store: The Storm store to load the schema into.
- :type store: storm.locals.Store`
- :param version: The schema version identifier of the form
- YYYYMMDDHHMMSS.
- :type version: string
- :param filename: The file name containing the schema to load. Pass
- `None` if there is no schema file to load.
- :type filename: string
- :param module_path: The fully qualified Python module path to the
- migration module being loaded. This is used to record information
- for use by the test suite.
- :type module_path: string
- """
- if filename is not None:
- contents = resource_string('mailman.database.schema', filename)
- self.load_sql(store, contents)
- # Add a marker that indicates the migration version being applied.
- store.add(Version(component='schema', version=version))
@staticmethod
def _make_temporary():
diff --git a/src/mailman/database/factory.py b/src/mailman/database/factory.py
index db453ea41..64fcc242c 100644
--- a/src/mailman/database/factory.py
+++ b/src/mailman/database/factory.py
@@ -62,10 +62,10 @@ class DatabaseFactory:
def _reset(self):
"""See `IDatabase`."""
- from mailman.database.model import ModelMeta
+ from mailman.database.model import Model
self.store.rollback()
self._pre_reset(self.store)
- ModelMeta._reset(self.store)
+ Model._reset(self)
self._post_reset(self.store)
self.store.commit()
diff --git a/src/mailman/database/model.py b/src/mailman/database/model.py
index ba2d39213..d86ebb80e 100644
--- a/src/mailman/database/model.py
+++ b/src/mailman/database/model.py
@@ -25,44 +25,24 @@ __all__ = [
]
+import contextlib
from operator import attrgetter
-from storm.properties import PropertyPublisherMeta
+from sqlalchemy.ext.declarative import declarative_base
+from mailman.config import config
-
-class ModelMeta(PropertyPublisherMeta):
+class ModelMeta(object):
"""Do more magic on table classes."""
- _class_registry = set()
-
- 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 so that the corresponding
- # table can be reset between tests.
- #
- # The PRESERVE flag indicates whether the table should be reset or
- # not. We have to handle the actual Model base class explicitly
- # because it does not correspond to a table in the database.
- if not getattr(self, 'PRESERVE', False) and name != 'Model':
- ModelMeta._class_registry.add(self)
-
@staticmethod
- def _reset(store):
- from mailman.config import config
- config.db._pre_reset(store)
- # Make sure this is deterministic, by sorting on the storm table name.
- classes = sorted(ModelMeta._class_registry,
- key=attrgetter('__storm_table__'))
- for model_class in classes:
- store.find(model_class).remove()
-
+ def _reset(db):
+ meta = Model.metadata
+ engine = config.db.engine
+ with contextlib.closing(engine.connect()) as con:
+ trans = con.begin()
+ for table in reversed(meta.sorted_tables):
+ con.execute(table.delete())
+ trans.commit()
-
-class Model:
- """Like Storm's `Storm` subclass, but with a bit extra."""
- __metaclass__ = ModelMeta
+Model = declarative_base(cls=ModelMeta)
diff --git a/src/mailman/database/postgresql.py b/src/mailman/database/postgresql.py
index 48c68a937..1ee454074 100644
--- a/src/mailman/database/postgresql.py
+++ b/src/mailman/database/postgresql.py
@@ -32,12 +32,12 @@ from functools import partial
from operator import attrgetter
from urlparse import urlsplit, urlunsplit
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
from mailman.testing.helpers import configuration
-class PostgreSQLDatabase(StormBaseDatabase):
+class PostgreSQLDatabase(SABaseDatabase):
"""Database class for PostgreSQL."""
TAG = 'postgres'
diff --git a/src/mailman/database/sqlite.py b/src/mailman/database/sqlite.py
index 15629615f..0594d9091 100644
--- a/src/mailman/database/sqlite.py
+++ b/src/mailman/database/sqlite.py
@@ -34,12 +34,12 @@ import tempfile
from functools import partial
from urlparse import urlparse
-from mailman.database.base import StormBaseDatabase
+from mailman.database.base import SABaseDatabase
from mailman.testing.helpers import configuration
-class SQLiteDatabase(StormBaseDatabase):
+class SQLiteDatabase(SABaseDatabase):
"""Database class for SQLite."""
TAG = 'sqlite'
@@ -72,7 +72,7 @@ def _cleanup(self, tempdir):
def make_temporary(database):
"""Adapts by monkey patching an existing SQLite IDatabase."""
tempdir = tempfile.mkdtemp()
- url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
+ url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
with configuration('database', url=url):
database.initialize()
database._cleanup = types.MethodType(
diff --git a/src/mailman/database/types.py b/src/mailman/database/types.py
index ba3d92df4..a6f0b32ca 100644
--- a/src/mailman/database/types.py
+++ b/src/mailman/database/types.py
@@ -23,43 +23,108 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Enum',
+ 'UUID',
]
+import uuid
-from storm.properties import SimpleProperty
-from storm.variables import Variable
+from sqlalchemy import Integer
+from sqlalchemy.types import TypeDecorator, BINARY, CHAR
+from sqlalchemy.dialects import postgresql
-class _EnumVariable(Variable):
- """Storm variable for supporting enum types.
-
- To use this, make the database column a INTEGER.
+class Enum(TypeDecorator):
+ """
+ Stores an integer-based Enum as an integer in the database, and converts it
+ on-the-fly.
"""
- def __init__(self, *args, **kws):
- self._enum = kws.pop('enum')
- super(_EnumVariable, self).__init__(*args, **kws)
+ impl = Integer
- def parse_set(self, value, from_db):
+ def __init__(self, *args, **kw):
+ self.enum = kw.pop("enum")
+ TypeDecorator.__init__(self, *args, **kw)
+
+ def process_bind_param(self, value, dialect):
if value is None:
return None
- if not from_db:
- return value
- return self._enum(value)
- def parse_get(self, value, to_db):
+ return value.value
+
+
+ def process_result_value(self, value, dialect):
if value is None:
return None
- if not to_db:
+ return self.enum(value)
+
+
+
+class UUID(TypeDecorator):
+ """
+ Stores a UUID in the database natively when it can and falls back to
+ a BINARY(16) or a CHAR(32) when it can't.
+
+ ::
+
+ from sqlalchemy_utils import UUIDType
+ import uuid
+
+ class User(Base):
+ __tablename__ = 'user'
+
+ # Pass `binary=False` to fallback to CHAR instead of BINARY
+ id = sa.Column(UUIDType(binary=False), primary_key=True)
+ """
+ impl = BINARY(16)
+
+ python_type = uuid.UUID
+
+ def __init__(self, binary=True, native=True):
+ """
+ :param binary: Whether to use a BINARY(16) or CHAR(32) fallback.
+ """
+ self.binary = binary
+ self.native = native
+
+ def load_dialect_impl(self, dialect):
+ if dialect.name == 'postgresql' and self.native:
+ # Use the native UUID type.
+ return dialect.type_descriptor(postgresql.UUID())
+
+ else:
+ # Fallback to either a BINARY or a CHAR.
+ kind = self.impl if self.binary else CHAR(32)
+ return dialect.type_descriptor(kind)
+
+ @staticmethod
+ def _coerce(value):
+ if value and not isinstance(value, uuid.UUID):
+ try:
+ value = uuid.UUID(value)
+
+ except (TypeError, ValueError):
+ value = uuid.UUID(bytes=value)
+
+ return value
+
+ def process_bind_param(self, value, dialect):
+ if value is None:
return value
- return value.value
+ if not isinstance(value, uuid.UUID):
+ value = self._coerce(value)
+
+ if self.native and dialect.name == 'postgresql':
+ return str(value)
-class Enum(SimpleProperty):
- """Custom type for Storm supporting enums."""
+ return value.bytes if self.binary else value.hex
+
+ def process_result_value(self, value, dialect):
+ if value is None:
+ return value
- variable_class = _EnumVariable
+ if self.native and dialect.name == 'postgresql':
+ return uuid.UUID(value)
- def __init__(self, enum=None):
- super(Enum, self).__init__(enum=enum)
+ return uuid.UUID(bytes=value) if self.binary else uuid.UUID(value)
diff --git a/src/mailman/interfaces/database.py b/src/mailman/interfaces/database.py
index 21f2f71d0..d8fde2b93 100644
--- a/src/mailman/interfaces/database.py
+++ b/src/mailman/interfaces/database.py
@@ -61,7 +61,7 @@ class IDatabase(Interface):
"""Abort the current transaction."""
store = Attribute(
- """The underlying Storm store on which you can do queries.""")
+ """The underlying SQLAlchemy store on which you can do queries.""")
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index f69679210..7203a31a5 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -26,7 +26,9 @@ __all__ = [
from email.utils import formataddr
-from storm.locals import DateTime, Int, Reference, Unicode
+from sqlalchemy import (Column, Integer, String, Unicode,
+ ForeignKey, DateTime)
+from sqlalchemy.orm import relationship, backref
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
@@ -42,20 +44,22 @@ 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'))
+
+ preferences_id = Column(Integer, ForeignKey('preferences.id'))
+ preferences = relationship('Preferences',
+ backref=backref('Address', uselist=False))
def __init__(self, email, display_name):
- super(Address, self).__init__()
getUtility(IEmailValidator).validate(email)
lower_case = email.lower()
self.email = lower_case
diff --git a/src/mailman/model/autorespond.py b/src/mailman/model/autorespond.py
index c5e736613..c3aff174a 100644
--- a/src/mailman/model/autorespond.py
+++ b/src/mailman/model/autorespond.py
@@ -26,7 +26,10 @@ __all__ = [
]
-from storm.locals import And, Date, Desc, Int, Reference
+from sqlalchemy import (Column, Integer, String, Unicode,
+ ForeignKey, Date)
+from sqlalchemy import desc
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from mailman.database.model import Model
@@ -42,16 +45,18 @@ from mailman.utilities.datetime import today
class AutoResponseRecord(Model):
"""See `IAutoResponseRecord`."""
- id = Int(primary=True)
+ __tablename__ = 'autorespondrecord'
- 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'))
+ address = relationship('Address')
- response_type = Enum(Response)
- date_sent = Date()
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+ mailing_list = relationship('MailingList')
+
+ response_type = Column(Enum(enum=Response))
+ date_sent = Column(Date)
def __init__(self, mailing_list, address, response_type):
self.mailing_list = mailing_list
@@ -71,12 +76,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 +92,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..fbbecaebd 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..1165fee96 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 Column, Integer, Unicode, DateTime, Boolean
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(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..e94bb073e 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'))
+ maling_list = relationship('MailingList')
- delivery_mode = Enum(DeliveryMode)
+ address_id = Column(Integer, ForeignKey('address.id'))
+ address = relationship('Address')
+
+ delivery_mode = Column(Enum(enum=DeliveryMode))
def __init__(self, mailing_list, address, delivery_mode):
self.mailing_list = mailing_list
diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py
index 28e346022..585eccf3d 100644
--- a/src/mailman/model/domain.py
+++ b/src/mailman/model/domain.py
@@ -27,7 +27,7 @@ __all__ = [
from urlparse import urljoin, urlparse
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Unicode, Integer
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)
+ base_url = Column(Unicode)
+ description = Column(Unicode)
+ contact_address = Column(Unicode)
def __init__(self, mail_host,
description=None,
@@ -92,8 +94,7 @@ class Domain(Model):
@dbconnection
def mailing_lists(self, store):
"""See `IDomain`."""
- mailing_lists = store.find(
- MailingList,
+ mailing_lists = store.query(MailingList).filter(
MailingList.mail_host == self.mail_host)
for mlist in mailing_lists:
yield mlist
@@ -140,14 +141,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 +165,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).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..7b611b6d8 100644
--- a/src/mailman/model/language.py
+++ b/src/mailman/model/language.py
@@ -25,8 +25,8 @@ __all__ = [
]
-from storm.locals import Int, Unicode
from zope.interface import implementer
+from sqlalchemy import Column, Unicode, Integer
from mailman.database import Model
from mailman.interfaces 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..3806f9497 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,40 @@ 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).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,7 +108,7 @@ 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
@@ -118,7 +116,7 @@ class ListManager:
@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..385262f28 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -27,9 +27,10 @@ __all__ = [
import os
-from storm.locals import (
- And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store,
- TimeDelta, Unicode)
+from sqlalchemy import (Column, Boolean, DateTime, Float, Integer, Unicode,
+ PickleType, Interval, ForeignKey, LargeBinary)
+from sqlalchemy import event
+from sqlalchemy.orm import relationship, sessionmaker
from urlparse import urljoin
from zope.component import getUtility
from zope.event import notify
@@ -67,130 +68,132 @@ from mailman.utilities.string import expand
SPACE = ' '
UNDERSCORE = '_'
+Session = sessionmaker()
@implementer(IMailingList)
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()
+ next_request_id = Column(Integer)
+ next_digest_number = Column(Integer)
+ digest_last_sent_at = Column(DateTime)
+ volume = Column(Integer)
+ last_post_at = Column(DateTime)
# Implicit destination.
- acceptable_aliases_id = Int()
- acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id')
+ # acceptable_aliases_id = Column(Integer, ForeignKey('acceptablealias.id'))
+ # acceptable_alias = relationship('AcceptableAlias', backref='mailing_list')
# 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(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(enum=ResponseAction))
+ autoresponse_owner_text = Column(Unicode)
+ autorespond_postings = Column(Enum(enum=ResponseAction))
+ autoresponse_postings_text = Column(Unicode)
+ autorespond_requests = Column(Enum(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(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(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(enum=Action))
+ default_nonmember_action = Column(Enum(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(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(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(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(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__()
listname, at, hostname = fqdn_listname.partition('@')
assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
self.list_name = listname
@@ -202,10 +205,11 @@ class MailingList(Model):
# 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__()
+ self._post_load()
makedirs(self.data_path)
- def __storm_loaded__(self):
+
+ def _post_load(self, *args):
self.owners = roster.OwnerRoster(self)
self.moderators = roster.ModeratorRoster(self)
self.administrators = roster.AdministratorRoster(self)
@@ -215,6 +219,10 @@ class MailingList(Model):
self.subscribers = roster.Subscribers(self)
self.nonmembers = roster.NonmemberRoster(self)
+ @classmethod
+ def __declare_last__(cls):
+ event.listen(cls, 'load', cls._post_load)
+
def __repr__(self):
return '<mailing list "{0}" at {1:#x}>'.format(
self.fqdn_listname, id(self))
@@ -326,26 +334,24 @@ class MailingList(Model):
def send_one_last_digest_to(self, address, delivery_mode):
"""See `IMailingList`."""
digest = OneLastDigest(self, address, delivery_mode)
- Store.of(self).add(digest)
+ Session.object_session(self).add(digest)
@property
def last_digest_recipients(self):
"""See `IMailingList`."""
- results = Store.of(self).find(
- OneLastDigest,
+ results = Session.object_session(self).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):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_mime))
+ results = Session.object_session(self).query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_mime)
for content_filter in results:
yield content_filter.filter_pattern
@@ -353,12 +359,11 @@ class MailingList(Model):
def filter_types(self, 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()
+ store = Session.object_session(self)
+ 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(
@@ -368,10 +373,9 @@ class MailingList(Model):
@property
def pass_types(self):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_mime))
+ results = Session.object_session(self).query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_mime)
for content_filter in results:
yield content_filter.filter_pattern
@@ -379,12 +383,11 @@ class MailingList(Model):
def pass_types(self, 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()
+ store = Session.object_session(self)
+ 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(
@@ -394,10 +397,9 @@ class MailingList(Model):
@property
def filter_extensions(self):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.filter_extension))
+ results = Session.object_session(self).query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.filter_extension)
for content_filter in results:
yield content_filter.filter_pattern
@@ -405,12 +407,11 @@ class MailingList(Model):
def filter_extensions(self, 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()
+ store = Session.object_session(self)
+ 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(
@@ -420,10 +421,9 @@ class MailingList(Model):
@property
def pass_extensions(self):
"""See `IMailingList`."""
- results = Store.of(self).find(
- ContentFilter,
- And(ContentFilter.mailing_list == self,
- ContentFilter.filter_type == FilterType.pass_extension))
+ results = Session.object_session(self).query(ContentFilter).filter(
+ ContentFilter.mailing_list == self,
+ ContentFilter.filter_type == FilterType.pass_extension)
for content_filter in results:
yield content_filter.pass_pattern
@@ -431,12 +431,11 @@ class MailingList(Model):
def pass_extensions(self, 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()
+ store = Session.object_session(self)
+ 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(
@@ -457,24 +456,22 @@ class MailingList(Model):
def subscribe(self, subscriber, role=MemberRole.member):
"""See `IMailingList`."""
- store = Store.of(self)
+ store = Session.object_session(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 +491,13 @@ 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'))
+ mailing_list = relationship('MailingList', backref='acceptable_alias')
+ alias = Column(Unicode)
def __init__(self, mailing_list, alias):
self.mailing_list = mailing_list
@@ -516,27 +514,27 @@ class AcceptableAliasSet:
def clear(self):
"""See `IAcceptableAliasSet`."""
- Store.of(self._mailing_list).find(
- AcceptableAlias,
- AcceptableAlias.mailing_list == self._mailing_list).remove()
+ Session.object_session(self._mailing_list).query(
+ AcceptableAlias).filter(
+ AcceptableAlias.mailing_list == self._mailing_list).delete()
def add(self, 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)
+ Session.object_session(self._mailing_list).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()
+ Session.object_session(self._mailing_list).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)
+ aliases = Session.object_session(self._mailing_list).query(
+ AcceptableAlias).filter(
+ AcceptableAlias.mailing_list_id == self._mailing_list.id)
for alias in aliases:
yield alias.alias
@@ -546,12 +544,14 @@ 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'))
+ mailing_list = relationship('MailingList')
+ name = Column(Unicode)
+ _is_enabled = Column(Boolean)
def __init__(self, mailing_list, archiver_name, system_archiver):
self.mailing_list = mailing_list
@@ -583,25 +583,24 @@ class ListArchiverSet:
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)
+ store = Session.object_session(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)
+ entries = Session.object_session(self._mailing_list).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()
+ return Session.object_session(self._mailing_list).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..f1007c311 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 Integer, Unicode, ForeignKey, Column
+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(enum=MemberRole))
+ list_id = Column(Unicode)
+ moderation_action = Column(Enum(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..39f33aa89 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, Unicode, LargeBinary
from zope.interface import implementer
from mailman.database.model import Model
@@ -37,15 +37,16 @@ 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)
+ message_id = Column(Unicode)
+ message_id_hash = Column(Unicode)
+ path = Column(LargeBinary) # TODO : was RawStr()
# This is a Messge-ID field representation, not a database row id.
@dbconnection
def __init__(self, store, message_id, message_id_hash, path):
- super(Message, self).__init__()
self.message_id = message_id
self.message_id_hash = message_id_hash
self.path = path
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index a4950e8c9..f9f224dd6 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -59,7 +59,7 @@ class MessageStore:
# 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(
@@ -107,7 +107,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)
@@ -120,7 +120,7 @@ class MessageStore:
# US-ASCII.
if isinstance(message_id_hash, unicode):
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 +128,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..3eac4f07b 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, Integer, Unicode, ForeignKey
+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'))
+ mailing_list = relationship('MailingList')
+
+ filter_type = Column(Enum(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..97d394721 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, Integer, Unicode, ForeignKey, DateTime, LargeBinary)
+from sqlalchemy.orm import relationship
from zope.interface import implementer
from zope.interface.verify import verifyObject
@@ -49,14 +51,16 @@ from mailman.utilities.modules import call_name
class PendedKeyValue(Model):
"""A pended key/value pair, tied to a token."""
+ __tablename__ = 'pendedkeyvalue'
+
def __init__(self, key, value):
self.key = key
self.value = value
- id = Int(primary=True)
- key = Unicode()
- value = Unicode()
- pended_id = Int()
+ id = Column(Integer, primary_key=True)
+ key = Column(Unicode)
+ value = Column(Unicode)
+ pended_id = Column(Integer, ForeignKey('pended.id'))
@@ -64,16 +68,16 @@ class PendedKeyValue(Model):
class Pended(Model):
"""A pended event, tied to a token."""
+ __tablename__ = 'pended'
+
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)
-
+ id = Column(Integer, primary_key=True)
+ token = Column(LargeBinary) # TODO : was RawStr()
+ expiration_date = Column(DateTime)
+ key_values = relationship('PendedKeyValue')
@implementer(IPendable)
@@ -105,7 +109,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')
@@ -129,7 +133,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 +141,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,7 +150,7 @@ 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,
+ for keyvalue in store.query(PendedKeyValue).filter(
PendedKeyValue.pended_id == pending.id):
if keyvalue.value is not None and '\1' in keyvalue.value:
type_name, value = keyvalue.value.split('\1', 1)
@@ -154,23 +158,23 @@ class Pendings:
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,
+ 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..d74b17e30 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 Column, Integer, Unicode, Boolean
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(enum=DeliveryMode))
+ delivery_status = Column(Enum(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..88ad0e407 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, Unicode, Integer, ForeignKey, LargeBinary
+from sqlalchemy.orm import relationship
from zope.component import getUtility
from zope.interface import implementer
@@ -68,25 +69,23 @@ 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 +103,12 @@ class ListRequests:
data_hash = token
request = _Request(key, request_type, self.mailing_list, data_hash)
store.add(request)
+ 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:
@@ -130,28 +130,29 @@ 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'
- mailing_list_id = Int()
- mailing_list = Reference(mailing_list_id, 'MailingList.id')
+ id = Column(Integer, primary_key=True)# TODO: ???, default=AutoReload)
+ key = Column(Unicode)
+ request_type = Column(Enum(enum=RequestType))
+ data_hash = Column(LargeBinary)
+
+ mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'))
+ mailing_list = relationship('MailingList')
def __init__(self, key, request_type, mailing_list, data_hash):
- super(_Request, self).__init__()
self.key = key
self.request_type = request_type
self.mailing_list = mailing_list
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 5a6a13269..a9a396523 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,19 +158,17 @@ 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,
+ or_(Member.role == MemberRole.owner,
Member.role == MemberRole.moderator))
@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,
- Or(Member.role == MemberRole.moderator,
+ or_(Member.role == MemberRole.moderator,
Member.role == MemberRole.owner),
Address.email == address,
Member.address_id == Address.id)
@@ -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..287a4dba5 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -29,7 +29,7 @@ __all__ = [
import unittest
-from storm.locals import Store
+from sqlalchemy.orm import sessionmaker
from zope.component import getUtility
from mailman.app.lifecycle import create_list
@@ -139,9 +139,9 @@ 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)
+ Session = sessionmaker()
+ store = Session.object_session(self._ant)
+ filters = store.query(ContentFilter).filter_by(mailing_list = self._ant)
self.assertEqual(filters.count(), 0)
diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py
index c60d0f1eb..77f1b59bb 100644
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -25,10 +25,11 @@ __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.types import UUID
from mailman.database.transaction import dbconnection
@@ -45,12 +46,14 @@ 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)
@dbconnection
def __init__(self, store, uid):
- super(UID, self).__init__()
self.uid = uid
store.add(self)
@@ -70,7 +73,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..cd47a5dac 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, Unicode, Integer, DateTime, ForeignKey, LargeBinary)
+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,36 @@ 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) # TODO : was RawStr()
+ _user_id = Column(UUID)
+ _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'))
+ _preferred_address = relationship('Address',
+ primaryjoin=
+ _preferred_address_id==Address.id,
+ post_update=True)
+
+ preferences_id = Column(Integer, ForeignKey('preferences.id'))
+ 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 +151,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 +161,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
index e99fb0d1c..95cf03dac 100644
--- a/src/mailman/model/version.py
+++ b/src/mailman/model/version.py
@@ -24,21 +24,24 @@ __all__ = [
'Version',
]
-from storm.locals import Int, Unicode
+from sqlalchemy import Column, Unicode, Integer
+
from mailman.database.model import Model
class Version(Model):
- id = Int(primary=True)
- component = Unicode()
- version = Unicode()
+
+ __tablename__ = 'version'
+
+ id = Column(Integer, primary_key=True)
+ component = Column(Unicode)
+ version = Column(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
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index cf22af24f..9856a8223 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -198,6 +198,14 @@ NAME_MAPPINGS = dict(
send_welcome_msg='send_welcome_message',
)
+# Datetime Fields that need a type conversion to python datetime
+# object for SQLite database.
+DATETIME_OBJECTS = [
+ 'created_at',
+ 'digest_last_sent_at',
+ 'last_post_time',
+]
+
EXCLUDES = set((
'digest_members',
'members',
@@ -217,6 +225,9 @@ def import_config_pck(mlist, config_dict):
# Some attributes must not be directly imported.
if key in EXCLUDES:
continue
+ # Created at must not be set, it needs a type conversion
+ if key in DATETIME_OBJECTS:
+ continue
# Some attributes from Mailman 2 were renamed in Mailman 3.
key = NAME_MAPPINGS.get(key, key)
# Handle the simple case where the key is an attribute of the
@@ -238,6 +249,15 @@ def import_config_pck(mlist, config_dict):
except (TypeError, KeyError):
print('Type conversion error for key "{}": {}'.format(
key, value), file=sys.stderr)
+ for key in DATETIME_OBJECTS:
+ try:
+ value = datetime.datetime.utcfromtimestamp(config_dict[key])
+ except KeyError:
+ continue
+ if key == 'last_post_time':
+ setattr(mlist, 'last_post_at', value)
+ continue
+ setattr(mlist, key, value)
# Handle the archiving policy. In MM2.1 there were two boolean options
# but only three of the four possible states were valid. Now there's just
# an enum.