~abompard/mailman/bug-1312884

« back to all changes in this revision

Viewing changes to src/mailman/model/mailinglist.py

  • Committer: Barry Warsaw
  • Date: 2014-11-01 16:49:15 UTC
  • mfrom: (7251.1.38 abhilash)
  • Revision ID: barry@list.org-20141101164915-06wqfmya6wf47n6n
Database
--------
 * The ORM layer, previously implemented with Storm, has been replaced by
   SQLAlchemy, thanks to the fantastic work by Abhilash Raj and Aurélien
   Bompard.  Alembic is now used for all database schema migrations.
 * The new logger `mailman.database` logs any errors at the database layer.

API
---
 * Several changes to the internal API:
   - `IListManager.mailing_lists` is guaranteed to be sorted in List-ID order.
   - `IDomains.mailing_lists` is guaranteed to be sorted in List-ID order.
   - Iteration over domains via the `IDomainManager` is guaranteed to be sorted
     by `IDomain.mail_host` order.
   - `ITemporaryDatabase` interface and all implementations are removed.

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
 
28
28
import os
29
29
 
30
 
from storm.locals import (
31
 
    And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store,
32
 
    TimeDelta, Unicode)
 
30
from sqlalchemy import (
 
31
    Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
 
32
    LargeBinary, PickleType, Unicode)
 
33
from sqlalchemy.event import listen
 
34
from sqlalchemy.orm import relationship
33
35
from urlparse import urljoin
34
36
from zope.component import getUtility
35
37
from zope.event import notify
37
39
 
38
40
from mailman.config import config
39
41
from mailman.database.model import Model
 
42
from mailman.database.transaction import dbconnection
40
43
from mailman.database.types import Enum
41
44
from mailman.interfaces.action import Action, FilterAction
42
45
from mailman.interfaces.address import IAddress
73
76
class MailingList(Model):
74
77
    """See `IMailingList`."""
75
78
 
76
 
    id = Int(primary=True)
 
79
    __tablename__ = 'mailinglist'
 
80
 
 
81
    id = Column(Integer, primary_key=True)
77
82
 
78
83
    # XXX denotes attributes that should be part of the public interface but
79
84
    # are currently missing.
80
85
 
81
86
    # List identity
82
 
    list_name = Unicode()
83
 
    mail_host = Unicode()
84
 
    _list_id = Unicode(name='list_id')
85
 
    allow_list_posts = Bool()
86
 
    include_rfc2369_headers = Bool()
87
 
    advertised = Bool()
88
 
    anonymous_list = Bool()
 
87
    list_name = Column(Unicode)
 
88
    mail_host = Column(Unicode)
 
89
    _list_id = Column('list_id', Unicode)
 
90
    allow_list_posts = Column(Boolean)
 
91
    include_rfc2369_headers = Column(Boolean)
 
92
    advertised = Column(Boolean)
 
93
    anonymous_list = Column(Boolean)
89
94
    # Attributes not directly modifiable via the web u/i
90
 
    created_at = DateTime()
91
 
    # Attributes which are directly modifiable via the web u/i.  The more
92
 
    # complicated attributes are currently stored as pickles, though that
93
 
    # will change as the schema and implementation is developed.
94
 
    next_request_id = Int()
95
 
    next_digest_number = Int()
96
 
    digest_last_sent_at = DateTime()
97
 
    volume = Int()
98
 
    last_post_at = DateTime()
99
 
    # Implicit destination.
100
 
    acceptable_aliases_id = Int()
101
 
    acceptable_alias = Reference(acceptable_aliases_id, 'AcceptableAlias.id')
102
 
    # Attributes which are directly modifiable via the web u/i.  The more
103
 
    # complicated attributes are currently stored as pickles, though that
104
 
    # will change as the schema and implementation is developed.
105
 
    accept_these_nonmembers = Pickle() # XXX
106
 
    admin_immed_notify = Bool()
107
 
    admin_notify_mchanges = Bool()
108
 
    administrivia = Bool()
109
 
    archive_policy = Enum(ArchivePolicy)
 
95
    created_at = Column(DateTime)
 
96
    # Attributes which are directly modifiable via the web u/i.  The more
 
97
    # complicated attributes are currently stored as pickles, though that
 
98
    # will change as the schema and implementation is developed.
 
99
    next_request_id = Column(Integer)
 
100
    next_digest_number = Column(Integer)
 
101
    digest_last_sent_at = Column(DateTime)
 
102
    volume = Column(Integer)
 
103
    last_post_at = Column(DateTime)
 
104
    # Attributes which are directly modifiable via the web u/i.  The more
 
105
    # complicated attributes are currently stored as pickles, though that
 
106
    # will change as the schema and implementation is developed.
 
107
    accept_these_nonmembers = Column(PickleType) # XXX
 
108
    admin_immed_notify = Column(Boolean)
 
109
    admin_notify_mchanges = Column(Boolean)
 
110
    administrivia = Column(Boolean)
 
111
    archive_policy = Column(Enum(ArchivePolicy))
110
112
    # Automatic responses.
111
 
    autoresponse_grace_period = TimeDelta()
112
 
    autorespond_owner = Enum(ResponseAction)
113
 
    autoresponse_owner_text = Unicode()
114
 
    autorespond_postings = Enum(ResponseAction)
115
 
    autoresponse_postings_text = Unicode()
116
 
    autorespond_requests = Enum(ResponseAction)
117
 
    autoresponse_request_text = Unicode()
 
113
    autoresponse_grace_period = Column(Interval)
 
114
    autorespond_owner = Column(Enum(ResponseAction))
 
115
    autoresponse_owner_text = Column(Unicode)
 
116
    autorespond_postings = Column(Enum(ResponseAction))
 
117
    autoresponse_postings_text = Column(Unicode)
 
118
    autorespond_requests = Column(Enum(ResponseAction))
 
119
    autoresponse_request_text = Column(Unicode)
118
120
    # Content filters.
119
 
    filter_action = Enum(FilterAction)
120
 
    filter_content = Bool()
121
 
    collapse_alternatives = Bool()
122
 
    convert_html_to_plaintext = Bool()
 
121
    filter_action = Column(Enum(FilterAction))
 
122
    filter_content = Column(Boolean)
 
123
    collapse_alternatives = Column(Boolean)
 
124
    convert_html_to_plaintext = Column(Boolean)
123
125
    # Bounces.
124
 
    bounce_info_stale_after = TimeDelta() # XXX
125
 
    bounce_matching_headers = Unicode() # XXX
126
 
    bounce_notify_owner_on_disable = Bool() # XXX
127
 
    bounce_notify_owner_on_removal = Bool() # XXX
128
 
    bounce_score_threshold = Int() # XXX
129
 
    bounce_you_are_disabled_warnings = Int() # XXX
130
 
    bounce_you_are_disabled_warnings_interval = TimeDelta() # XXX
131
 
    forward_unrecognized_bounces_to = Enum(UnrecognizedBounceDisposition)
132
 
    process_bounces = Bool()
 
126
    bounce_info_stale_after = Column(Interval) # XXX
 
127
    bounce_matching_headers = Column(Unicode) # XXX
 
128
    bounce_notify_owner_on_disable = Column(Boolean) # XXX
 
129
    bounce_notify_owner_on_removal = Column(Boolean) # XXX
 
130
    bounce_score_threshold = Column(Integer) # XXX
 
131
    bounce_you_are_disabled_warnings = Column(Integer) # XXX
 
132
    bounce_you_are_disabled_warnings_interval = Column(Interval) # XXX
 
133
    forward_unrecognized_bounces_to = Column(
 
134
        Enum(UnrecognizedBounceDisposition))
 
135
    process_bounces = Column(Boolean)
133
136
    # Miscellaneous
134
 
    default_member_action = Enum(Action)
135
 
    default_nonmember_action = Enum(Action)
136
 
    description = Unicode()
137
 
    digest_footer_uri = Unicode()
138
 
    digest_header_uri = Unicode()
139
 
    digest_is_default = Bool()
140
 
    digest_send_periodic = Bool()
141
 
    digest_size_threshold = Float()
142
 
    digest_volume_frequency = Enum(DigestFrequency)
143
 
    digestable = Bool()
144
 
    discard_these_nonmembers = Pickle()
145
 
    emergency = Bool()
146
 
    encode_ascii_prefixes = Bool()
147
 
    first_strip_reply_to = Bool()
148
 
    footer_uri = Unicode()
149
 
    forward_auto_discards = Bool()
150
 
    gateway_to_mail = Bool()
151
 
    gateway_to_news = Bool()
152
 
    goodbye_message_uri = Unicode()
153
 
    header_matches = Pickle()
154
 
    header_uri = Unicode()
155
 
    hold_these_nonmembers = Pickle()
156
 
    info = Unicode()
157
 
    linked_newsgroup = Unicode()
158
 
    max_days_to_hold = Int()
159
 
    max_message_size = Int()
160
 
    max_num_recipients = Int()
161
 
    member_moderation_notice = Unicode()
162
 
    mime_is_default_digest = Bool()
 
137
    default_member_action = Column(Enum(Action))
 
138
    default_nonmember_action = Column(Enum(Action))
 
139
    description = Column(Unicode)
 
140
    digest_footer_uri = Column(Unicode)
 
141
    digest_header_uri = Column(Unicode)
 
142
    digest_is_default = Column(Boolean)
 
143
    digest_send_periodic = Column(Boolean)
 
144
    digest_size_threshold = Column(Float)
 
145
    digest_volume_frequency = Column(Enum(DigestFrequency))
 
146
    digestable = Column(Boolean)
 
147
    discard_these_nonmembers = Column(PickleType)
 
148
    emergency = Column(Boolean)
 
149
    encode_ascii_prefixes = Column(Boolean)
 
150
    first_strip_reply_to = Column(Boolean)
 
151
    footer_uri = Column(Unicode)
 
152
    forward_auto_discards = Column(Boolean)
 
153
    gateway_to_mail = Column(Boolean)
 
154
    gateway_to_news = Column(Boolean)
 
155
    goodbye_message_uri = Column(Unicode)
 
156
    header_matches = Column(PickleType)
 
157
    header_uri = Column(Unicode)
 
158
    hold_these_nonmembers = Column(PickleType)
 
159
    info = Column(Unicode)
 
160
    linked_newsgroup = Column(Unicode)
 
161
    max_days_to_hold = Column(Integer)
 
162
    max_message_size = Column(Integer)
 
163
    max_num_recipients = Column(Integer)
 
164
    member_moderation_notice = Column(Unicode)
 
165
    mime_is_default_digest = Column(Boolean)
163
166
    # FIXME: There should be no moderator_password
164
 
    moderator_password = RawStr()
165
 
    newsgroup_moderation = Enum(NewsgroupModeration)
166
 
    nntp_prefix_subject_too = Bool()
167
 
    nondigestable = Bool()
168
 
    nonmember_rejection_notice = Unicode()
169
 
    obscure_addresses = Bool()
170
 
    owner_chain = Unicode()
171
 
    owner_pipeline = Unicode()
172
 
    personalize = Enum(Personalization)
173
 
    post_id = Int()
174
 
    posting_chain = Unicode()
175
 
    posting_pipeline = Unicode()
176
 
    _preferred_language = Unicode(name='preferred_language')
177
 
    display_name = Unicode()
178
 
    reject_these_nonmembers = Pickle()
179
 
    reply_goes_to_list = Enum(ReplyToMunging)
180
 
    reply_to_address = Unicode()
181
 
    require_explicit_destination = Bool()
182
 
    respond_to_post_requests = Bool()
183
 
    scrub_nondigest = Bool()
184
 
    send_goodbye_message = Bool()
185
 
    send_welcome_message = Bool()
186
 
    subject_prefix = Unicode()
187
 
    topics = Pickle()
188
 
    topics_bodylines_limit = Int()
189
 
    topics_enabled = Bool()
190
 
    welcome_message_uri = Unicode()
 
167
    moderator_password = Column(LargeBinary) # TODO : was RawStr()
 
168
    newsgroup_moderation = Column(Enum(NewsgroupModeration))
 
169
    nntp_prefix_subject_too = Column(Boolean)
 
170
    nondigestable = Column(Boolean)
 
171
    nonmember_rejection_notice = Column(Unicode)
 
172
    obscure_addresses = Column(Boolean)
 
173
    owner_chain = Column(Unicode)
 
174
    owner_pipeline = Column(Unicode)
 
175
    personalize = Column(Enum(Personalization))
 
176
    post_id = Column(Integer)
 
177
    posting_chain = Column(Unicode)
 
178
    posting_pipeline = Column(Unicode)
 
179
    _preferred_language = Column('preferred_language', Unicode)
 
180
    display_name = Column(Unicode)
 
181
    reject_these_nonmembers = Column(PickleType)
 
182
    reply_goes_to_list = Column(Enum(ReplyToMunging))
 
183
    reply_to_address = Column(Unicode)
 
184
    require_explicit_destination = Column(Boolean)
 
185
    respond_to_post_requests = Column(Boolean)
 
186
    scrub_nondigest = Column(Boolean)
 
187
    send_goodbye_message = Column(Boolean)
 
188
    send_welcome_message = Column(Boolean)
 
189
    subject_prefix = Column(Unicode)
 
190
    topics = Column(PickleType)
 
191
    topics_bodylines_limit = Column(Integer)
 
192
    topics_enabled = Column(Boolean)
 
193
    welcome_message_uri = Column(Unicode)
191
194
 
192
195
    def __init__(self, fqdn_listname):
193
196
        super(MailingList, self).__init__()
198
201
        self._list_id = '{0}.{1}'.format(listname, hostname)
199
202
        # For the pending database
200
203
        self.next_request_id = 1
201
 
        # We need to set up the rosters.  Normally, this method will get
202
 
        # called when the MailingList object is loaded from the database, but
203
 
        # that's not the case when the constructor is called.  So, set up the
204
 
        # rosters explicitly.
205
 
        self.__storm_loaded__()
 
204
        # We need to set up the rosters.  Normally, this method will get called
 
205
        # when the MailingList object is loaded from the database, but when the
 
206
        # constructor is called, SQLAlchemy's `load` event isn't triggered.
 
207
        # Thus we need to set up the rosters explicitly.
 
208
        self._post_load()
206
209
        makedirs(self.data_path)
207
210
 
208
 
    def __storm_loaded__(self):
 
211
    def _post_load(self, *args):
 
212
        # This hooks up to SQLAlchemy's `load` event.
209
213
        self.owners = roster.OwnerRoster(self)
210
214
        self.moderators = roster.ModeratorRoster(self)
211
215
        self.administrators = roster.AdministratorRoster(self)
215
219
        self.subscribers = roster.Subscribers(self)
216
220
        self.nonmembers = roster.NonmemberRoster(self)
217
221
 
 
222
    @classmethod
 
223
    def __declare_last__(cls):
 
224
        # SQLAlchemy special directive hook called after mappings are assumed
 
225
        # to be complete.  Use this to connect the roster instance creation
 
226
        # method with the SA `load` event.
 
227
        listen(cls, 'load', cls._post_load)
 
228
 
218
229
    def __repr__(self):
219
230
        return '<mailing list "{0}" at {1:#x}>'.format(
220
231
            self.fqdn_listname, id(self))
323
334
        except AttributeError:
324
335
            self._preferred_language = language
325
336
 
326
 
    def send_one_last_digest_to(self, address, delivery_mode):
 
337
    @dbconnection
 
338
    def send_one_last_digest_to(self, store, address, delivery_mode):
327
339
        """See `IMailingList`."""
328
340
        digest = OneLastDigest(self, address, delivery_mode)
329
 
        Store.of(self).add(digest)
 
341
        store.add(digest)
330
342
 
331
343
    @property
332
 
    def last_digest_recipients(self):
 
344
    @dbconnection
 
345
    def last_digest_recipients(self, store):
333
346
        """See `IMailingList`."""
334
 
        results = Store.of(self).find(
335
 
            OneLastDigest,
 
347
        results = store.query(OneLastDigest).filter(
336
348
            OneLastDigest.mailing_list == self)
337
349
        recipients = [(digest.address, digest.delivery_mode)
338
350
                      for digest in results]
339
 
        results.remove()
 
351
        results.delete()
340
352
        return recipients
341
353
 
342
354
    @property
343
 
    def filter_types(self):
 
355
    @dbconnection
 
356
    def filter_types(self, store):
344
357
        """See `IMailingList`."""
345
 
        results = Store.of(self).find(
346
 
            ContentFilter,
347
 
            And(ContentFilter.mailing_list == self,
348
 
                ContentFilter.filter_type == FilterType.filter_mime))
 
358
        results = store.query(ContentFilter).filter(
 
359
            ContentFilter.mailing_list == self,
 
360
            ContentFilter.filter_type == FilterType.filter_mime)
349
361
        for content_filter in results:
350
362
            yield content_filter.filter_pattern
351
363
 
352
364
    @filter_types.setter
353
 
    def filter_types(self, sequence):
 
365
    @dbconnection
 
366
    def filter_types(self, store, sequence):
354
367
        """See `IMailingList`."""
355
368
        # First, delete all existing MIME type filter patterns.
356
 
        store = Store.of(self)
357
 
        results = store.find(
358
 
            ContentFilter,
359
 
            And(ContentFilter.mailing_list == self,
360
 
                ContentFilter.filter_type == FilterType.filter_mime))
361
 
        results.remove()
 
369
        results = store.query(ContentFilter).filter(
 
370
            ContentFilter.mailing_list == self,
 
371
            ContentFilter.filter_type == FilterType.filter_mime)
 
372
        results.delete()
362
373
        # Now add all the new filter types.
363
374
        for mime_type in sequence:
364
375
            content_filter = ContentFilter(
366
377
            store.add(content_filter)
367
378
 
368
379
    @property
369
 
    def pass_types(self):
 
380
    @dbconnection
 
381
    def pass_types(self, store):
370
382
        """See `IMailingList`."""
371
 
        results = Store.of(self).find(
372
 
            ContentFilter,
373
 
            And(ContentFilter.mailing_list == self,
374
 
                ContentFilter.filter_type == FilterType.pass_mime))
 
383
        results = store.query(ContentFilter).filter(
 
384
            ContentFilter.mailing_list == self,
 
385
            ContentFilter.filter_type == FilterType.pass_mime)
375
386
        for content_filter in results:
376
387
            yield content_filter.filter_pattern
377
388
 
378
389
    @pass_types.setter
379
 
    def pass_types(self, sequence):
 
390
    @dbconnection
 
391
    def pass_types(self, store, sequence):
380
392
        """See `IMailingList`."""
381
393
        # First, delete all existing MIME type pass patterns.
382
 
        store = Store.of(self)
383
 
        results = store.find(
384
 
            ContentFilter,
385
 
            And(ContentFilter.mailing_list == self,
386
 
                ContentFilter.filter_type == FilterType.pass_mime))
387
 
        results.remove()
 
394
        results = store.query(ContentFilter).filter(
 
395
            ContentFilter.mailing_list == self,
 
396
            ContentFilter.filter_type == FilterType.pass_mime)
 
397
        results.delete()
388
398
        # Now add all the new filter types.
389
399
        for mime_type in sequence:
390
400
            content_filter = ContentFilter(
392
402
            store.add(content_filter)
393
403
 
394
404
    @property
395
 
    def filter_extensions(self):
 
405
    @dbconnection
 
406
    def filter_extensions(self, store):
396
407
        """See `IMailingList`."""
397
 
        results = Store.of(self).find(
398
 
            ContentFilter,
399
 
            And(ContentFilter.mailing_list == self,
400
 
                ContentFilter.filter_type == FilterType.filter_extension))
 
408
        results = store.query(ContentFilter).filter(
 
409
            ContentFilter.mailing_list == self,
 
410
            ContentFilter.filter_type == FilterType.filter_extension)
401
411
        for content_filter in results:
402
412
            yield content_filter.filter_pattern
403
413
 
404
414
    @filter_extensions.setter
405
 
    def filter_extensions(self, sequence):
 
415
    @dbconnection
 
416
    def filter_extensions(self, store, sequence):
406
417
        """See `IMailingList`."""
407
418
        # First, delete all existing file extensions filter patterns.
408
 
        store = Store.of(self)
409
 
        results = store.find(
410
 
            ContentFilter,
411
 
            And(ContentFilter.mailing_list == self,
412
 
                ContentFilter.filter_type == FilterType.filter_extension))
413
 
        results.remove()
 
419
        results = store.query(ContentFilter).filter(
 
420
            ContentFilter.mailing_list == self,
 
421
            ContentFilter.filter_type == FilterType.filter_extension)
 
422
        results.delete()
414
423
        # Now add all the new filter types.
415
424
        for mime_type in sequence:
416
425
            content_filter = ContentFilter(
418
427
            store.add(content_filter)
419
428
 
420
429
    @property
421
 
    def pass_extensions(self):
 
430
    @dbconnection
 
431
    def pass_extensions(self, store):
422
432
        """See `IMailingList`."""
423
 
        results = Store.of(self).find(
424
 
            ContentFilter,
425
 
            And(ContentFilter.mailing_list == self,
426
 
                ContentFilter.filter_type == FilterType.pass_extension))
 
433
        results = store.query(ContentFilter).filter(
 
434
            ContentFilter.mailing_list == self,
 
435
            ContentFilter.filter_type == FilterType.pass_extension)
427
436
        for content_filter in results:
428
437
            yield content_filter.pass_pattern
429
438
 
430
439
    @pass_extensions.setter
431
 
    def pass_extensions(self, sequence):
 
440
    @dbconnection
 
441
    def pass_extensions(self, store, sequence):
432
442
        """See `IMailingList`."""
433
443
        # First, delete all existing file extensions pass patterns.
434
 
        store = Store.of(self)
435
 
        results = store.find(
436
 
            ContentFilter,
437
 
            And(ContentFilter.mailing_list == self,
438
 
                ContentFilter.filter_type == FilterType.pass_extension))
439
 
        results.remove()
 
444
        results = store.query(ContentFilter).filter(
 
445
            ContentFilter.mailing_list == self,
 
446
            ContentFilter.filter_type == FilterType.pass_extension)
 
447
        results.delete()
440
448
        # Now add all the new filter types.
441
449
        for mime_type in sequence:
442
450
            content_filter = ContentFilter(
452
460
        elif role is MemberRole.moderator:
453
461
            return self.moderators
454
462
        else:
455
 
            raise TypeError(
456
 
                'Undefined MemberRole: {0}'.format(role))
 
463
            raise TypeError('Undefined MemberRole: {}'.format(role))
457
464
 
458
 
    def subscribe(self, subscriber, role=MemberRole.member):
 
465
    @dbconnection
 
466
    def subscribe(self, store, subscriber, role=MemberRole.member):
459
467
        """See `IMailingList`."""
460
 
        store = Store.of(self)
461
468
        if IAddress.providedBy(subscriber):
462
 
            member = store.find(
463
 
                Member,
 
469
            member = store.query(Member).filter(
464
470
                Member.role == role,
465
471
                Member.list_id == self._list_id,
466
 
                Member._address == subscriber).one()
 
472
                Member._address == subscriber).first()
467
473
            if member:
468
474
                raise AlreadySubscribedError(
469
475
                    self.fqdn_listname, subscriber.email, role)
470
476
        elif IUser.providedBy(subscriber):
471
477
            if subscriber.preferred_address is None:
472
478
                raise MissingPreferredAddressError(subscriber)
473
 
            member = store.find(
474
 
                Member,
 
479
            member = store.query(Member).filter(
475
480
                Member.role == role,
476
481
                Member.list_id == self._list_id,
477
 
                Member._user == subscriber).one()
 
482
                Member._user == subscriber).first()
478
483
            if member:
479
484
                raise AlreadySubscribedError(
480
485
                    self.fqdn_listname, subscriber, role)
494
499
class AcceptableAlias(Model):
495
500
    """See `IAcceptableAlias`."""
496
501
 
497
 
    id = Int(primary=True)
498
 
 
499
 
    mailing_list_id = Int()
500
 
    mailing_list = Reference(mailing_list_id, MailingList.id)
501
 
 
502
 
    alias = Unicode()
 
502
    __tablename__ = 'acceptablealias'
 
503
 
 
504
    id = Column(Integer, primary_key=True)
 
505
 
 
506
    mailing_list_id = Column(
 
507
        Integer, ForeignKey('mailinglist.id'),
 
508
        index=True, nullable=False)
 
509
    mailing_list = relationship('MailingList', backref='acceptable_alias')
 
510
    alias = Column(Unicode, index=True, nullable=False)
503
511
 
504
512
    def __init__(self, mailing_list, alias):
505
513
        self.mailing_list = mailing_list
514
522
    def __init__(self, mailing_list):
515
523
        self._mailing_list = mailing_list
516
524
 
517
 
    def clear(self):
 
525
    @dbconnection
 
526
    def clear(self, store):
518
527
        """See `IAcceptableAliasSet`."""
519
 
        Store.of(self._mailing_list).find(
520
 
            AcceptableAlias,
521
 
            AcceptableAlias.mailing_list == self._mailing_list).remove()
 
528
        store.query(AcceptableAlias).filter(
 
529
            AcceptableAlias.mailing_list == self._mailing_list).delete()
522
530
 
523
 
    def add(self, alias):
 
531
    @dbconnection
 
532
    def add(self, store, alias):
524
533
        if not (alias.startswith('^') or '@' in alias):
525
534
            raise ValueError(alias)
526
535
        alias = AcceptableAlias(self._mailing_list, alias.lower())
527
 
        Store.of(self._mailing_list).add(alias)
 
536
        store.add(alias)
528
537
 
529
 
    def remove(self, alias):
530
 
        Store.of(self._mailing_list).find(
531
 
            AcceptableAlias,
532
 
            And(AcceptableAlias.mailing_list == self._mailing_list,
533
 
                AcceptableAlias.alias == alias.lower())).remove()
 
538
    @dbconnection
 
539
    def remove(self, store, alias):
 
540
        store.query(AcceptableAlias).filter(
 
541
            AcceptableAlias.mailing_list == self._mailing_list,
 
542
            AcceptableAlias.alias == alias.lower()).delete()
534
543
 
535
544
    @property
536
 
    def aliases(self):
537
 
        aliases = Store.of(self._mailing_list).find(
538
 
            AcceptableAlias,
539
 
            AcceptableAlias.mailing_list == self._mailing_list)
 
545
    @dbconnection
 
546
    def aliases(self, store):
 
547
        aliases = store.query(AcceptableAlias).filter(
 
548
            AcceptableAlias.mailing_list_id == self._mailing_list.id)
540
549
        for alias in aliases:
541
550
            yield alias.alias
542
551
 
546
555
class ListArchiver(Model):
547
556
    """See `IListArchiver`."""
548
557
 
549
 
    id = Int(primary=True)
550
 
 
551
 
    mailing_list_id = Int()
552
 
    mailing_list = Reference(mailing_list_id, MailingList.id)
553
 
    name = Unicode()
554
 
    _is_enabled = Bool()
 
558
    __tablename__ = 'listarchiver'
 
559
 
 
560
    id = Column(Integer, primary_key=True)
 
561
 
 
562
    mailing_list_id = Column(
 
563
        Integer, ForeignKey('mailinglist.id'),
 
564
        index=True, nullable=False)
 
565
    mailing_list = relationship('MailingList')
 
566
 
 
567
    name = Column(Unicode, nullable=False)
 
568
    _is_enabled = Column(Boolean)
555
569
 
556
570
    def __init__(self, mailing_list, archiver_name, system_archiver):
557
571
        self.mailing_list = mailing_list
576
590
 
577
591
@implementer(IListArchiverSet)
578
592
class ListArchiverSet:
579
 
    def __init__(self, mailing_list):
 
593
    @dbconnection
 
594
    def __init__(self, store, mailing_list):
580
595
        self._mailing_list = mailing_list
581
596
        system_archivers = {}
582
597
        for archiver in config.archivers:
583
598
            system_archivers[archiver.name] = archiver
584
599
        # Add any system enabled archivers which aren't already associated
585
600
        # with the mailing list.
586
 
        store = Store.of(self._mailing_list)
587
601
        for archiver_name in system_archivers:
588
 
            exists = store.find(
589
 
                ListArchiver,
590
 
                And(ListArchiver.mailing_list == mailing_list,
591
 
                    ListArchiver.name == archiver_name)).one()
 
602
            exists = store.query(ListArchiver).filter(
 
603
                ListArchiver.mailing_list == mailing_list,
 
604
                ListArchiver.name == archiver_name).first()
592
605
            if exists is None:
593
606
                store.add(ListArchiver(mailing_list, archiver_name,
594
607
                                       system_archivers[archiver_name]))
595
608
 
596
609
    @property
597
 
    def archivers(self):
598
 
        entries = Store.of(self._mailing_list).find(
599
 
            ListArchiver, ListArchiver.mailing_list == self._mailing_list)
 
610
    @dbconnection
 
611
    def archivers(self, store):
 
612
        entries = store.query(ListArchiver).filter(
 
613
            ListArchiver.mailing_list == self._mailing_list)
600
614
        for entry in entries:
601
615
            yield entry
602
616
 
603
 
    def get(self, archiver_name):
604
 
        return Store.of(self._mailing_list).find(
605
 
            ListArchiver,
606
 
            And(ListArchiver.mailing_list == self._mailing_list,
607
 
                ListArchiver.name == archiver_name)).one()
 
617
    @dbconnection
 
618
    def get(self, store, archiver_name):
 
619
        return store.query(ListArchiver).filter(
 
620
            ListArchiver.mailing_list == self._mailing_list,
 
621
            ListArchiver.name == archiver_name).first()