diff options
| author | Barry Warsaw | 2014-11-01 12:49:15 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2014-11-01 12:49:15 -0400 |
| commit | 8ab9c5111a05277e185b5e038bf12e13cd6df15e (patch) | |
| tree | 9307b9f2fb65a90bc4d61a2c97478b582a96de87 /src/mailman/model/mailinglist.py | |
| parent | b6bc505e45a2f1f4f99d7dd2cdd868d533270ee9 (diff) | |
| parent | fb38e482aa42edd4032a23e7c1f727066991fa62 (diff) | |
| download | mailman-8ab9c5111a05277e185b5e038bf12e13cd6df15e.tar.gz mailman-8ab9c5111a05277e185b5e038bf12e13cd6df15e.tar.zst mailman-8ab9c5111a05277e185b5e038bf12e13cd6df15e.zip | |
Diffstat (limited to 'src/mailman/model/mailinglist.py')
| -rw-r--r-- | src/mailman/model/mailinglist.py | 432 |
1 files changed, 223 insertions, 209 deletions
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 955a76968..761a78b94 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -27,9 +27,11 @@ __all__ = [ import os -from storm.locals import ( - And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store, - TimeDelta, Unicode) +from sqlalchemy import ( + Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval, + LargeBinary, PickleType, Unicode) +from sqlalchemy.event import listen +from sqlalchemy.orm import relationship from urlparse import urljoin from zope.component import getUtility from zope.event import notify @@ -37,6 +39,7 @@ from zope.interface import implementer from mailman.config import config from mailman.database.model import Model +from mailman.database.transaction import dbconnection from mailman.database.types import Enum from mailman.interfaces.action import Action, FilterAction from mailman.interfaces.address import IAddress @@ -73,121 +76,121 @@ UNDERSCORE = '_' class MailingList(Model): """See `IMailingList`.""" - id = Int(primary=True) + __tablename__ = 'mailinglist' + + id = Column(Integer, primary_key=True) # XXX denotes attributes that should be part of the public interface but # are currently missing. # List identity - list_name = Unicode() - mail_host = Unicode() - _list_id = Unicode(name='list_id') - allow_list_posts = Bool() - include_rfc2369_headers = Bool() - advertised = Bool() - anonymous_list = Bool() + list_name = Column(Unicode) + mail_host = Column(Unicode) + _list_id = Column('list_id', Unicode) + allow_list_posts = Column(Boolean) + include_rfc2369_headers = Column(Boolean) + advertised = Column(Boolean) + anonymous_list = Column(Boolean) # Attributes not directly modifiable via the web u/i - created_at = DateTime() + created_at = Column(DateTime) # Attributes which are directly modifiable via the web u/i. The more # complicated attributes are currently stored as pickles, though that # will change as the schema and implementation is developed. - next_request_id = Int() - next_digest_number = Int() - digest_last_sent_at = DateTime() - volume = Int() - last_post_at = DateTime() - # Implicit destination. - acceptable_aliases_id = Int() - acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id') + next_request_id = Column(Integer) + next_digest_number = Column(Integer) + digest_last_sent_at = Column(DateTime) + volume = Column(Integer) + last_post_at = Column(DateTime) # Attributes which are directly modifiable via the web u/i. The more # complicated attributes are currently stored as pickles, though that # will change as the schema and implementation is developed. - accept_these_nonmembers = Pickle() # XXX - admin_immed_notify = Bool() - admin_notify_mchanges = Bool() - administrivia = Bool() - archive_policy = Enum(ArchivePolicy) + accept_these_nonmembers = Column(PickleType) # XXX + admin_immed_notify = Column(Boolean) + admin_notify_mchanges = Column(Boolean) + administrivia = Column(Boolean) + archive_policy = Column(Enum(ArchivePolicy)) # Automatic responses. - autoresponse_grace_period = TimeDelta() - autorespond_owner = Enum(ResponseAction) - autoresponse_owner_text = Unicode() - autorespond_postings = Enum(ResponseAction) - autoresponse_postings_text = Unicode() - autorespond_requests = Enum(ResponseAction) - autoresponse_request_text = Unicode() + autoresponse_grace_period = Column(Interval) + autorespond_owner = Column(Enum(ResponseAction)) + autoresponse_owner_text = Column(Unicode) + autorespond_postings = Column(Enum(ResponseAction)) + autoresponse_postings_text = Column(Unicode) + autorespond_requests = Column(Enum(ResponseAction)) + autoresponse_request_text = Column(Unicode) # Content filters. - filter_action = Enum(FilterAction) - filter_content = Bool() - collapse_alternatives = Bool() - convert_html_to_plaintext = Bool() + filter_action = Column(Enum(FilterAction)) + filter_content = Column(Boolean) + collapse_alternatives = Column(Boolean) + convert_html_to_plaintext = Column(Boolean) # Bounces. - bounce_info_stale_after = TimeDelta() # XXX - bounce_matching_headers = Unicode() # XXX - bounce_notify_owner_on_disable = Bool() # XXX - bounce_notify_owner_on_removal = Bool() # XXX - bounce_score_threshold = Int() # XXX - bounce_you_are_disabled_warnings = Int() # XXX - bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX - forward_unrecognized_bounces_to = Enum(UnrecognizedBounceDisposition) - process_bounces = Bool() + bounce_info_stale_after = Column(Interval) # XXX + bounce_matching_headers = Column(Unicode) # XXX + bounce_notify_owner_on_disable = Column(Boolean) # XXX + bounce_notify_owner_on_removal = Column(Boolean) # XXX + bounce_score_threshold = Column(Integer) # XXX + bounce_you_are_disabled_warnings = Column(Integer) # XXX + bounce_you_are_disabled_warnings_interval = Column(Interval) # XXX + forward_unrecognized_bounces_to = Column( + Enum(UnrecognizedBounceDisposition)) + process_bounces = Column(Boolean) # Miscellaneous - default_member_action = Enum(Action) - default_nonmember_action = Enum(Action) - description = Unicode() - digest_footer_uri = Unicode() - digest_header_uri = Unicode() - digest_is_default = Bool() - digest_send_periodic = Bool() - digest_size_threshold = Float() - digest_volume_frequency = Enum(DigestFrequency) - digestable = Bool() - discard_these_nonmembers = Pickle() - emergency = Bool() - encode_ascii_prefixes = Bool() - first_strip_reply_to = Bool() - footer_uri = Unicode() - forward_auto_discards = Bool() - gateway_to_mail = Bool() - gateway_to_news = Bool() - goodbye_message_uri = Unicode() - header_matches = Pickle() - header_uri = Unicode() - hold_these_nonmembers = Pickle() - info = Unicode() - linked_newsgroup = Unicode() - max_days_to_hold = Int() - max_message_size = Int() - max_num_recipients = Int() - member_moderation_notice = Unicode() - mime_is_default_digest = Bool() + default_member_action = Column(Enum(Action)) + default_nonmember_action = Column(Enum(Action)) + description = Column(Unicode) + digest_footer_uri = Column(Unicode) + digest_header_uri = Column(Unicode) + digest_is_default = Column(Boolean) + digest_send_periodic = Column(Boolean) + digest_size_threshold = Column(Float) + digest_volume_frequency = Column(Enum(DigestFrequency)) + digestable = Column(Boolean) + discard_these_nonmembers = Column(PickleType) + emergency = Column(Boolean) + encode_ascii_prefixes = Column(Boolean) + first_strip_reply_to = Column(Boolean) + footer_uri = Column(Unicode) + forward_auto_discards = Column(Boolean) + gateway_to_mail = Column(Boolean) + gateway_to_news = Column(Boolean) + goodbye_message_uri = Column(Unicode) + header_matches = Column(PickleType) + header_uri = Column(Unicode) + hold_these_nonmembers = Column(PickleType) + info = Column(Unicode) + linked_newsgroup = Column(Unicode) + max_days_to_hold = Column(Integer) + max_message_size = Column(Integer) + max_num_recipients = Column(Integer) + member_moderation_notice = Column(Unicode) + mime_is_default_digest = Column(Boolean) # FIXME: There should be no moderator_password - moderator_password = RawStr() - newsgroup_moderation = Enum(NewsgroupModeration) - nntp_prefix_subject_too = Bool() - nondigestable = Bool() - nonmember_rejection_notice = Unicode() - obscure_addresses = Bool() - owner_chain = Unicode() - owner_pipeline = Unicode() - personalize = Enum(Personalization) - post_id = Int() - posting_chain = Unicode() - posting_pipeline = Unicode() - _preferred_language = Unicode(name='preferred_language') - display_name = Unicode() - reject_these_nonmembers = Pickle() - reply_goes_to_list = Enum(ReplyToMunging) - reply_to_address = Unicode() - require_explicit_destination = Bool() - respond_to_post_requests = Bool() - scrub_nondigest = Bool() - send_goodbye_message = Bool() - send_welcome_message = Bool() - subject_prefix = Unicode() - topics = Pickle() - topics_bodylines_limit = Int() - topics_enabled = Bool() - welcome_message_uri = Unicode() + moderator_password = Column(LargeBinary) # TODO : was RawStr() + newsgroup_moderation = Column(Enum(NewsgroupModeration)) + nntp_prefix_subject_too = Column(Boolean) + nondigestable = Column(Boolean) + nonmember_rejection_notice = Column(Unicode) + obscure_addresses = Column(Boolean) + owner_chain = Column(Unicode) + owner_pipeline = Column(Unicode) + personalize = Column(Enum(Personalization)) + post_id = Column(Integer) + posting_chain = Column(Unicode) + posting_pipeline = Column(Unicode) + _preferred_language = Column('preferred_language', Unicode) + display_name = Column(Unicode) + reject_these_nonmembers = Column(PickleType) + reply_goes_to_list = Column(Enum(ReplyToMunging)) + reply_to_address = Column(Unicode) + require_explicit_destination = Column(Boolean) + respond_to_post_requests = Column(Boolean) + scrub_nondigest = Column(Boolean) + send_goodbye_message = Column(Boolean) + send_welcome_message = Column(Boolean) + subject_prefix = Column(Unicode) + topics = Column(PickleType) + topics_bodylines_limit = Column(Integer) + topics_enabled = Column(Boolean) + welcome_message_uri = Column(Unicode) def __init__(self, fqdn_listname): super(MailingList, self).__init__() @@ -198,14 +201,15 @@ class MailingList(Model): self._list_id = '{0}.{1}'.format(listname, hostname) # For the pending database self.next_request_id = 1 - # We need to set up the rosters. Normally, this method will get - # called when the MailingList object is loaded from the database, but - # that's not the case when the constructor is called. So, set up the - # rosters explicitly. - self.__storm_loaded__() + # We need to set up the rosters. Normally, this method will get called + # when the MailingList object is loaded from the database, but when the + # constructor is called, SQLAlchemy's `load` event isn't triggered. + # Thus we need to set up the rosters explicitly. + self._post_load() makedirs(self.data_path) - def __storm_loaded__(self): + def _post_load(self, *args): + # This hooks up to SQLAlchemy's `load` event. self.owners = roster.OwnerRoster(self) self.moderators = roster.ModeratorRoster(self) self.administrators = roster.AdministratorRoster(self) @@ -215,6 +219,13 @@ class MailingList(Model): self.subscribers = roster.Subscribers(self) self.nonmembers = roster.NonmemberRoster(self) + @classmethod + def __declare_last__(cls): + # SQLAlchemy special directive hook called after mappings are assumed + # to be complete. Use this to connect the roster instance creation + # method with the SA `load` event. + listen(cls, 'load', cls._post_load) + def __repr__(self): return '<mailing list "{0}" at {1:#x}>'.format( self.fqdn_listname, id(self)) @@ -323,42 +334,42 @@ class MailingList(Model): except AttributeError: self._preferred_language = language - def send_one_last_digest_to(self, address, delivery_mode): + @dbconnection + def send_one_last_digest_to(self, store, address, delivery_mode): """See `IMailingList`.""" digest = OneLastDigest(self, address, delivery_mode) - Store.of(self).add(digest) + store.add(digest) @property - def last_digest_recipients(self): + @dbconnection + def last_digest_recipients(self, store): """See `IMailingList`.""" - results = Store.of(self).find( - OneLastDigest, + results = store.query(OneLastDigest).filter( OneLastDigest.mailing_list == self) recipients = [(digest.address, digest.delivery_mode) for digest in results] - results.remove() + results.delete() return recipients @property - def filter_types(self): + @dbconnection + def filter_types(self, store): """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_mime)) + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.filter_mime) for content_filter in results: yield content_filter.filter_pattern @filter_types.setter - def filter_types(self, sequence): + @dbconnection + def filter_types(self, store, sequence): """See `IMailingList`.""" # First, delete all existing MIME type filter patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_mime)) - results.remove() + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.filter_mime) + results.delete() # Now add all the new filter types. for mime_type in sequence: content_filter = ContentFilter( @@ -366,25 +377,24 @@ class MailingList(Model): store.add(content_filter) @property - def pass_types(self): + @dbconnection + def pass_types(self, store): """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_mime)) + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.pass_mime) for content_filter in results: yield content_filter.filter_pattern @pass_types.setter - def pass_types(self, sequence): + @dbconnection + def pass_types(self, store, sequence): """See `IMailingList`.""" # First, delete all existing MIME type pass patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_mime)) - results.remove() + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.pass_mime) + results.delete() # Now add all the new filter types. for mime_type in sequence: content_filter = ContentFilter( @@ -392,25 +402,24 @@ class MailingList(Model): store.add(content_filter) @property - def filter_extensions(self): + @dbconnection + def filter_extensions(self, store): """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_extension)) + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.filter_extension) for content_filter in results: yield content_filter.filter_pattern @filter_extensions.setter - def filter_extensions(self, sequence): + @dbconnection + def filter_extensions(self, store, sequence): """See `IMailingList`.""" # First, delete all existing file extensions filter patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.filter_extension)) - results.remove() + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.filter_extension) + results.delete() # Now add all the new filter types. for mime_type in sequence: content_filter = ContentFilter( @@ -418,25 +427,24 @@ class MailingList(Model): store.add(content_filter) @property - def pass_extensions(self): + @dbconnection + def pass_extensions(self, store): """See `IMailingList`.""" - results = Store.of(self).find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_extension)) + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.pass_extension) for content_filter in results: yield content_filter.pass_pattern @pass_extensions.setter - def pass_extensions(self, sequence): + @dbconnection + def pass_extensions(self, store, sequence): """See `IMailingList`.""" # First, delete all existing file extensions pass patterns. - store = Store.of(self) - results = store.find( - ContentFilter, - And(ContentFilter.mailing_list == self, - ContentFilter.filter_type == FilterType.pass_extension)) - results.remove() + results = store.query(ContentFilter).filter( + ContentFilter.mailing_list == self, + ContentFilter.filter_type == FilterType.pass_extension) + results.delete() # Now add all the new filter types. for mime_type in sequence: content_filter = ContentFilter( @@ -452,29 +460,26 @@ class MailingList(Model): elif role is MemberRole.moderator: return self.moderators else: - raise TypeError( - 'Undefined MemberRole: {0}'.format(role)) + raise TypeError('Undefined MemberRole: {}'.format(role)) - def subscribe(self, subscriber, role=MemberRole.member): + @dbconnection + def subscribe(self, store, subscriber, role=MemberRole.member): """See `IMailingList`.""" - store = Store.of(self) if IAddress.providedBy(subscriber): - member = store.find( - Member, + member = store.query(Member).filter( Member.role == role, Member.list_id == self._list_id, - Member._address == subscriber).one() + Member._address == subscriber).first() if member: raise AlreadySubscribedError( self.fqdn_listname, subscriber.email, role) elif IUser.providedBy(subscriber): if subscriber.preferred_address is None: raise MissingPreferredAddressError(subscriber) - member = store.find( - Member, + member = store.query(Member).filter( Member.role == role, Member.list_id == self._list_id, - Member._user == subscriber).one() + Member._user == subscriber).first() if member: raise AlreadySubscribedError( self.fqdn_listname, subscriber, role) @@ -494,12 +499,15 @@ class MailingList(Model): class AcceptableAlias(Model): """See `IAcceptableAlias`.""" - id = Int(primary=True) + __tablename__ = 'acceptablealias' - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, MailingList.id) + id = Column(Integer, primary_key=True) - alias = Unicode() + mailing_list_id = Column( + Integer, ForeignKey('mailinglist.id'), + index=True, nullable=False) + mailing_list = relationship('MailingList', backref='acceptable_alias') + alias = Column(Unicode, index=True, nullable=False) def __init__(self, mailing_list, alias): self.mailing_list = mailing_list @@ -514,29 +522,30 @@ class AcceptableAliasSet: def __init__(self, mailing_list): self._mailing_list = mailing_list - def clear(self): + @dbconnection + def clear(self, store): """See `IAcceptableAliasSet`.""" - Store.of(self._mailing_list).find( - AcceptableAlias, - AcceptableAlias.mailing_list == self._mailing_list).remove() + store.query(AcceptableAlias).filter( + AcceptableAlias.mailing_list == self._mailing_list).delete() - def add(self, alias): + @dbconnection + def add(self, store, alias): if not (alias.startswith('^') or '@' in alias): raise ValueError(alias) alias = AcceptableAlias(self._mailing_list, alias.lower()) - Store.of(self._mailing_list).add(alias) + store.add(alias) - def remove(self, alias): - Store.of(self._mailing_list).find( - AcceptableAlias, - And(AcceptableAlias.mailing_list == self._mailing_list, - AcceptableAlias.alias == alias.lower())).remove() + @dbconnection + def remove(self, store, alias): + store.query(AcceptableAlias).filter( + AcceptableAlias.mailing_list == self._mailing_list, + AcceptableAlias.alias == alias.lower()).delete() @property - def aliases(self): - aliases = Store.of(self._mailing_list).find( - AcceptableAlias, - AcceptableAlias.mailing_list == self._mailing_list) + @dbconnection + def aliases(self, store): + aliases = store.query(AcceptableAlias).filter( + AcceptableAlias.mailing_list_id == self._mailing_list.id) for alias in aliases: yield alias.alias @@ -546,12 +555,17 @@ class AcceptableAliasSet: class ListArchiver(Model): """See `IListArchiver`.""" - id = Int(primary=True) + __tablename__ = 'listarchiver' + + id = Column(Integer, primary_key=True) - mailing_list_id = Int() - mailing_list = Reference(mailing_list_id, MailingList.id) - name = Unicode() - _is_enabled = Bool() + mailing_list_id = Column( + Integer, ForeignKey('mailinglist.id'), + index=True, nullable=False) + mailing_list = relationship('MailingList') + + name = Column(Unicode, nullable=False) + _is_enabled = Column(Boolean) def __init__(self, mailing_list, archiver_name, system_archiver): self.mailing_list = mailing_list @@ -576,32 +590,32 @@ class ListArchiver(Model): @implementer(IListArchiverSet) class ListArchiverSet: - def __init__(self, mailing_list): + @dbconnection + def __init__(self, store, mailing_list): self._mailing_list = mailing_list system_archivers = {} for archiver in config.archivers: system_archivers[archiver.name] = archiver # Add any system enabled archivers which aren't already associated # with the mailing list. - store = Store.of(self._mailing_list) for archiver_name in system_archivers: - exists = store.find( - ListArchiver, - And(ListArchiver.mailing_list == mailing_list, - ListArchiver.name == archiver_name)).one() + exists = store.query(ListArchiver).filter( + ListArchiver.mailing_list == mailing_list, + ListArchiver.name == archiver_name).first() if exists is None: store.add(ListArchiver(mailing_list, archiver_name, system_archivers[archiver_name])) @property - def archivers(self): - entries = Store.of(self._mailing_list).find( - ListArchiver, ListArchiver.mailing_list == self._mailing_list) + @dbconnection + def archivers(self, store): + entries = store.query(ListArchiver).filter( + ListArchiver.mailing_list == self._mailing_list) for entry in entries: yield entry - def get(self, archiver_name): - return Store.of(self._mailing_list).find( - ListArchiver, - And(ListArchiver.mailing_list == self._mailing_list, - ListArchiver.name == archiver_name)).one() + @dbconnection + def get(self, store, archiver_name): + return store.query(ListArchiver).filter( + ListArchiver.mailing_list == self._mailing_list, + ListArchiver.name == archiver_name).first() |
