diff options
Diffstat (limited to 'Mailman/database')
| -rw-r--r-- | Mailman/database/__init__.py | 50 | ||||
| -rw-r--r-- | Mailman/database/listmanager.py | 25 | ||||
| -rw-r--r-- | Mailman/database/messagestore.py | 31 | ||||
| -rw-r--r-- | Mailman/database/model/__init__.py | 47 | ||||
| -rw-r--r-- | Mailman/database/model/address.py | 37 | ||||
| -rw-r--r-- | Mailman/database/model/language.py | 8 | ||||
| -rw-r--r-- | Mailman/database/model/mailinglist.py | 233 | ||||
| -rw-r--r-- | Mailman/database/model/mailman.sql | 206 | ||||
| -rw-r--r-- | Mailman/database/model/member.py | 34 | ||||
| -rw-r--r-- | Mailman/database/model/message.py | 20 | ||||
| -rw-r--r-- | Mailman/database/model/pending.py | 85 | ||||
| -rw-r--r-- | Mailman/database/model/preferences.py | 27 | ||||
| -rw-r--r-- | Mailman/database/model/requests.py | 73 | ||||
| -rw-r--r-- | Mailman/database/model/roster.py | 48 | ||||
| -rw-r--r-- | Mailman/database/model/user.py | 30 | ||||
| -rw-r--r-- | Mailman/database/model/version.py | 15 | ||||
| -rw-r--r-- | Mailman/database/types.py | 54 | ||||
| -rw-r--r-- | Mailman/database/usermanager.py | 36 |
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 |
