From c7f8019ff5967a4f65d831ba77188435ed575b83 Mon Sep 17 00:00:00 2001 From: Joanna Skrzeszewska Date: Sun, 8 Sep 2013 07:22:32 +0000 Subject: Changes for enabling/disabling archivers. --- src/mailman/interfaces/mailinglist.py | 11 +++++++ src/mailman/model/mailinglist.py | 59 ++++++++++++++++++++++++++++++++--- src/mailman/rest/configuration.py | 29 ++++++++++++++++- src/mailman/runners/archive.py | 31 +++++++++--------- 4 files changed, 111 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 7beaf9c46..8519238db 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -24,6 +24,7 @@ __all__ = [ 'IAcceptableAlias', 'IAcceptableAliasSet', 'IMailingList', + 'IArchiverList', 'Personalization', 'ReplyToMunging', ] @@ -54,6 +55,16 @@ class ReplyToMunging(Enum): # An explicit Reply-To header is added explicit_header = 2 +class IArchiverList(Interface): + mailing_list_id = Attribute("""List id""") + archiver_name = Attribute("""Archiver name""") + archiver_enabled = Attribute("""If is enabled.""") + +class IListArchiverSet(Interface): + def getAll(): + """Return dict containing all archivers and their settings.""" + def set(archiver, is_enabled): + """Set archiver for this list.""" class IMailingList(Interface): diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index dd7a528c3..a0239caa9 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -28,8 +28,8 @@ __all__ = [ import os from storm.locals import ( - And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store, - TimeDelta, Unicode) + And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, ReferenceSet, + Store, TimeDelta, Unicode) from urlparse import urljoin from zope.component import getUtility from zope.event import notify @@ -47,8 +47,8 @@ from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.domain import IDomainManager from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.mailinglist import ( - IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization, - ReplyToMunging) + IAcceptableAlias, IAcceptableAliasSet, IArchiverList, IListArchiverSet, + IMailingList, Personalization, ReplyToMunging) from mailman.interfaces.member import ( AlreadySubscribedError, MemberRole, MissingPreferredAddressError, SubscriptionEvent) @@ -67,6 +67,19 @@ from mailman.utilities.string import expand SPACE = ' ' UNDERSCORE = '_' +@implementer(IArchiverList) +class ArchiverList(Model): + __storm_primary__ = "mailing_list_id", "archiver_name" + mailing_list_id = Int() + archiver_name = Unicode() + archiver_enabled = Bool() + + def __init__(self, mailing_list_id, archiver_name): + self.mailing_list_id = mailing_list_id + self.archiver_name = archiver_name + self.archiver_enabled = False + + @implementer(IMailingList) @@ -78,6 +91,7 @@ class MailingList(Model): # XXX denotes attributes that should be part of the public interface but # are currently missing. + archivers = ReferenceSet(id, ArchiverList.mailing_list_id) # List identity list_name = Unicode() mail_host = Unicode() @@ -538,3 +552,40 @@ class AcceptableAliasSet: AcceptableAlias.mailing_list == self._mailing_list) for alias in aliases: yield alias.alias + +@implementer(IListArchiverSet) +class ListArchiverSet: + def __init__(self, mailing_list): + self._mailing_list = mailing_list + self.lazyAdd() + + def getAll(self): + entries = Store.of(self._mailing_list).find(ArchiverList, ArchiverList.mailing_list_id == self._mailing_list.id) + all_in_config = {archiver.name for archiver in config.archivers} + ret = {} + for entry in entries: + if entry.archiver_name in all_in_config: + ret[entry.archiver_name] = int(entry.archiver_enabled) + return ret + + def set(self, archiver, is_enabled): + bool_enabled = (int(is_enabled) != 0) + self.get(archiver).set(archiver_enabled=bool_enabled) + + def isEnabled(self, archiverName): + return self.get(archiverName).one().archiver_enabled + + def get(self, archiverName): + return Store.of(self._mailing_list).find(ArchiverList, + (ArchiverList.mailing_list_id == self._mailing_list.id) & (ArchiverList.archiver_name == archiverName)) + + def lazyAdd(self): + names = [] + for archiver in config.archivers: + count = self.get(archiver.name).count() + names.append((archiver.name, count)) + if not count: + entry = ArchiverList(self._mailing_list.id, archiver.name) + Store.of(self._mailing_list).add(entry) + Store.of(self._mailing_list).commit() + diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py index c726d8a81..eec248ad0 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/configuration.py @@ -34,9 +34,10 @@ from mailman.core.errors import ( from mailman.interfaces.action import Action from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.autorespond import ResponseAction -from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging +from mailman.interfaces.mailinglist import IAcceptableAliasSet, IListArchiverSet, ReplyToMunging from mailman.rest.helpers import GetterSetter, PATCH, etag, no_content from mailman.rest.validator import PatchValidator, Validator, enum_validator +from mailman.model.mailinglist import ListArchiverSet @@ -64,6 +65,22 @@ class AcceptableAliases(GetterSetter): for alias in value: alias_set.add(unicode(alias)) +class ListArchivers(GetterSetter): + + def get(self, mlist, attribute): + """Return the mailing list's acceptable aliases.""" + assert attribute == 'archivers', ( + 'Unexpected attribute: {0}'.format(attribute)) + archivers = ListArchiverSet(mlist) + return archivers.getAll() + + def put(self, mlist, attribute, value): + assert attribute == 'archivers', ( + 'Unexpected attribute: {0}'.format(attribute)) + archivers = ListArchiverSet(mlist) + for key, value in value.iteritems(): + archivers.set(key, value) + # Additional validators for converting from web request strings to internal @@ -80,6 +97,15 @@ def list_of_unicode(values): """Turn a list of things into a list of unicodes.""" return [unicode(value) for value in values] +def list_of_pairs_to_dict(pairs): + dict = {} + # If pairs has only one element then it is not a list but a string. + if not isinstance(pairs, list): + pairs = [pairs] + for key_value in pairs: + parts = key_value.split('|') + dict[parts[0]] = parts[1] + return dict # This is the list of IMailingList attributes that are exposed through the @@ -98,6 +124,7 @@ def list_of_unicode(values): ATTRIBUTES = dict( acceptable_aliases=AcceptableAliases(list_of_unicode), + archivers=ListArchivers(list_of_pairs_to_dict), admin_immed_notify=GetterSetter(as_boolean), admin_notify_mchanges=GetterSetter(as_boolean), administrivia=GetterSetter(as_boolean), diff --git a/src/mailman/runners/archive.py b/src/mailman/runners/archive.py index f18bd7c61..9eb1dd0a2 100644 --- a/src/mailman/runners/archive.py +++ b/src/mailman/runners/archive.py @@ -36,6 +36,7 @@ from mailman.config import config from mailman.core.runner import Runner from mailman.interfaces.archiver import ClobberDate from mailman.utilities.datetime import RFC822_DATE_FMT, now +from mailman.model.mailinglist import ListArchiverSet log = logging.getLogger('mailman.error') @@ -91,17 +92,19 @@ class ArchiveRunner(Runner): def _dispose(self, mlist, msg, msgdata): received_time = msgdata.get('received_time', now(strip_tzinfo=False)) for archiver in config.archivers: - msg_copy = copy.deepcopy(msg) - if _should_clobber(msg, msgdata, archiver.name): - original_date = msg_copy['date'] - del msg_copy['date'] - del msg_copy['x-original-date'] - msg_copy['Date'] = received_time.strftime(RFC822_DATE_FMT) - if original_date: - msg_copy['X-Original-Date'] = original_date - # A problem in one archiver should not prevent other archivers - # from running. - try: - archiver.archive_message(mlist, msg_copy) - except Exception: - log.exception('Broken archiver: %s' % archiver.name) + archSet = ListArchiverSet(mlist) + if archSet.isEnabled(archiver.name): + msg_copy = copy.deepcopy(msg) + if _should_clobber(msg, msgdata, archiver.name): + original_date = msg_copy['date'] + del msg_copy['date'] + del msg_copy['x-original-date'] + msg_copy['Date'] = received_time.strftime(RFC822_DATE_FMT) + if original_date: + msg_copy['X-Original-Date'] = original_date + # A problem in one archiver should not prevent other archivers + # from running. + try: + archiver.archive_message(mlist, msg_copy) + except Exception: + log.exception('Broken archiver: %s' % archiver.name) -- cgit v1.3.1 From 39b25c3285b5f6cefd7498412e9722b7b062eab2 Mon Sep 17 00:00:00 2001 From: Joanna Skrzeszewska Date: Wed, 18 Sep 2013 10:35:30 +0000 Subject: Unit tests for enabling/disabling archivers. --- src/mailman/rest/tests/test_lists.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src') diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py index 7c2182c9c..1c11865e6 100644 --- a/src/mailman/rest/tests/test_lists.py +++ b/src/mailman/rest/tests/test_lists.py @@ -28,12 +28,16 @@ __all__ = [ import unittest +from zope.component import getUtility from urllib2 import HTTPError from zope.component import getUtility from mailman.app.lifecycle import create_list +from mailman.config import config from mailman.database.transaction import transaction from mailman.interfaces.usermanager import IUserManager +from mailman.interfaces.listmanager import IListManager +from mailman.model.mailinglist import ListArchiverSet from mailman.testing.helpers import call_api from mailman.testing.layers import RESTLayer @@ -159,3 +163,25 @@ class TestLists(unittest.TestCase): call_api('http://localhost:9001/3.0/lists/ant.example.com', method='DELETE') self.assertEqual(cm.exception.code, 404) + + def test_prototype_in_list_archivers(self): + resource, response = call_api( + 'http://localhost:9001/3.0/lists/test@example.com/config') + self.assertEqual(response.status, 200) + self.assertEqual(resource['archivers']['prototype'], 0) + + def test_lazy_add_archivers(self): + call_api('http://localhost:9001/3.0/lists', { + 'fqdn_listname': 'new_list@example.com', + }) + resource, response = call_api( + 'http://localhost:9001/3.0/lists/new_list@example.com/config') + self.assertEqual(response.status, 200) + self.assertEqual(resource['archivers']['prototype'], 0) + + def test_set_archiver_enabled(self): + mlist = getUtility(IListManager).create('newest_list@example.com') + lset = ListArchiverSet(mlist) + lset.set('prototype', 1) + self.assertEqual(lset.isEnabled('prototype'), 1) + -- cgit v1.3.1 From 833633741f110354408b3e0cfd7f733a2541e97c Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 25 Nov 2013 21:26:15 -0500 Subject: Checkpointing. --- src/mailman/archiving/mailarchive.py | 1 + src/mailman/archiving/mhonarc.py | 1 + src/mailman/archiving/prototype.py | 1 + src/mailman/config/config.py | 10 +- src/mailman/config/configure.zcml | 6 ++ src/mailman/config/schema.cfg | 2 +- src/mailman/config/tests/test_archivers.py | 56 +++++++++++ .../database/schema/sqlite_20130406000000_01.sql | 20 +++- src/mailman/database/tests/test_migrations.py | 27 ++++++ src/mailman/interfaces/archiver.py | 2 + src/mailman/interfaces/listmanager.py | 8 +- src/mailman/interfaces/mailinglist.py | 47 ++++++--- src/mailman/model/mailinglist.py | 107 ++++++++++++--------- src/mailman/model/tests/test_domain.py | 2 + src/mailman/model/tests/test_listmanager.py | 5 +- src/mailman/model/tests/test_mailinglist.py | 102 ++++++++++++++++++++ src/mailman/rest/configuration.py | 16 +-- src/mailman/runners/archive.py | 39 ++++---- src/mailman/runners/tests/test_archiver.py | 14 +++ 19 files changed, 373 insertions(+), 93 deletions(-) create mode 100644 src/mailman/config/tests/test_archivers.py create mode 100644 src/mailman/model/tests/test_mailinglist.py (limited to 'src') diff --git a/src/mailman/archiving/mailarchive.py b/src/mailman/archiving/mailarchive.py index 34a10fd25..a8489d02e 100644 --- a/src/mailman/archiving/mailarchive.py +++ b/src/mailman/archiving/mailarchive.py @@ -43,6 +43,7 @@ class MailArchive: """ name = 'mail-archive' + is_enabled = False def __init__(self): # Read our specific configuration file diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py index 6f8f3e168..646030f5e 100644 --- a/src/mailman/archiving/mhonarc.py +++ b/src/mailman/archiving/mhonarc.py @@ -46,6 +46,7 @@ class MHonArc: """Local MHonArc archiver.""" name = 'mhonarc' + is_enabled = False def __init__(self): # Read our specific configuration file diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py index df215a0da..60fa4c59f 100644 --- a/src/mailman/archiving/prototype.py +++ b/src/mailman/archiving/prototype.py @@ -52,6 +52,7 @@ class Prototype: """ name = 'prototype' + is_enabled = False @staticmethod def list_url(mlist): diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py index 74931c029..86919e3f1 100644 --- a/src/mailman/config/config.py +++ b/src/mailman/config/config.py @@ -249,12 +249,14 @@ class Configuration: @property def archivers(self): - """Iterate over all the enabled archivers.""" + """Iterate over all the archivers.""" for section in self._config.getByCategory('archiver', []): - if not as_boolean(section.enable): + class_path = section['class'].strip() + if len(class_path) == 0: continue - class_path = section['class'] - yield call_name(class_path) + archiver = call_name(class_path) + archiver.is_enabled = as_boolean(section.enable) + yield archiver @property def style_configs(self): diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index efb449538..f9b9cb093 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -28,6 +28,12 @@ factory="mailman.model.mailinglist.AcceptableAliasSet" /> + + . + +"""Site-wide archiver configuration tests.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + 'TestArchivers', + ] + + +import unittest + +from mailman.config import config +from mailman.testing.helpers import configuration +from mailman.testing.layers import ConfigLayer + + + +class TestArchivers(unittest.TestCase): + layer = ConfigLayer + + def test_enabled(self): + # By default, the testing configuration enables the archivers. + archivers = {} + for archiver in config.archivers: + archivers[archiver.name] = archiver + self.assertTrue(archivers['prototype'].is_enabled) + self.assertTrue(archivers['mail-archive'].is_enabled) + self.assertTrue(archivers['mhonarc'].is_enabled) + + @configuration('archiver.mhonarc', enable='no') + def test_disabled(self): + # We just disabled one of the archivers. + archivers = {} + for archiver in config.archivers: + archivers[archiver.name] = archiver + self.assertTrue(archivers['prototype'].is_enabled) + self.assertTrue(archivers['mail-archive'].is_enabled) + self.assertFalse(archivers['mhonarc'].is_enabled) diff --git a/src/mailman/database/schema/sqlite_20130406000000_01.sql b/src/mailman/database/schema/sqlite_20130406000000_01.sql index 9bdc2aae0..fe30ed247 100644 --- a/src/mailman/database/schema/sqlite_20130406000000_01.sql +++ b/src/mailman/database/schema/sqlite_20130406000000_01.sql @@ -6,12 +6,17 @@ -- For SQLite3 migration strategy, see -- http://sqlite.org/faq.html#q11 --- REMOVALS from the bounceevent table: +-- ADD listarchiver table. + +-- REMOVALs from the bounceevent table: -- REM list_name --- ADDS to the ban bounceevent table: +-- ADDs to the bounceevent table: -- ADD list_id +-- ADDs to the mailinglist table: +-- ADD archiver_id + CREATE TABLE bounceevent_backup ( id INTEGER NOT NULL, email TEXT, @@ -28,3 +33,14 @@ INSERT INTO bounceevent_backup SELECT FROM bounceevent; ALTER TABLE bounceevent_backup ADD COLUMN list_id TEXT; + +CREATE TABLE listarchiver ( + id INTEGER NOT NULL, + mailing_list_id INTEGER NOT NULL, + name TEXT NOT NULL, + _is_enabled BOOLEAN, + PRIMARY KEY (id) + ); + +CREATE INDEX ix_listarchiver_mailing_list_id + ON listarchiver(mailing_list_id); diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py index d983f9891..44f594ba7 100644 --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -36,6 +36,7 @@ import unittest from datetime import datetime from operator import attrgetter from pkg_resources import resource_string +from sqlite3 import OperationalError from storm.exceptions import DatabaseError from zope.component import getUtility @@ -65,6 +66,23 @@ class MigrationTestBase(unittest.TestCase): def tearDown(self): self._database._cleanup() + def _table_missing_present(self, migrations, missing, present): + """The appropriate migrations leave some tables missing and present. + + :param migrations: Sequence of migrations to load. + :param missing: Tables which should be missing. + :param present: Tables which should be present. + """ + for migration in migrations: + self._database.load_migrations(migration) + self._database.store.commit() + for table in missing: + self.assertRaises(OperationalError, + self._database.store.execute, + 'select * from {};'.format(table)) + for table in present: + self._database.store.execute('select * from {};'.format(table)) + def _missing_present(self, table, migrations, missing, present): """The appropriate migrations leave columns missing and present. @@ -450,6 +468,15 @@ class TestMigration20130406Schema(MigrationTestBase): ('list_name',), ('list_id',)) + def test_pre_listarchiver_table(self): + self._table_missing_present(['20130405999999'], ('listarchiver',), ()) + + def test_post_listarchiver_table(self): + self._table_missing_present(['20130405999999', + '20130406000000'], + (), + ('listarchiver',)) + class TestMigration20130406MigratedData(MigrationTestBase): diff --git a/src/mailman/interfaces/archiver.py b/src/mailman/interfaces/archiver.py index 5f074503e..aac372865 100644 --- a/src/mailman/interfaces/archiver.py +++ b/src/mailman/interfaces/archiver.py @@ -50,6 +50,8 @@ class IArchiver(Interface): """An interface to the archiver.""" name = Attribute('The name of this archiver') + is_enabled = Attribute( + 'A flag indicating whether this archiver is enabled site-wide.') def list_url(mlist): """Return the url to the top of the list's archive. diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py index 45b12af53..837abf310 100644 --- a/src/mailman/interfaces/listmanager.py +++ b/src/mailman/interfaces/listmanager.py @@ -97,9 +97,9 @@ class IListManager(Interface): def create(fqdn_listname): """Create a mailing list with the given name. - :type fqdn_listname: Unicode :param fqdn_listname: The fully qualified name of the mailing list, e.g. `mylist@example.com`. + :type fqdn_listname: Unicode :return: The newly created `IMailingList`. :raise `ListAlreadyExistsError` if the named list already exists. """ @@ -107,8 +107,8 @@ class IListManager(Interface): def get(fqdn_listname): """Return the mailing list with the given name, if it exists. - :type fqdn_listname: Unicode. :param fqdn_listname: The fully qualified name of the mailing list. + :type fqdn_listname: Unicode. :return: the matching `IMailingList` or None if the named list does not exist. """ @@ -116,8 +116,8 @@ class IListManager(Interface): def get_by_list_id(list_id): """Return the mailing list with the given list id, if it exists. - :type fqdn_listname: Unicode. :param fqdn_listname: The fully qualified name of the mailing list. + :type fqdn_listname: Unicode. :return: the matching `IMailingList` or None if the named list does not exist. """ @@ -125,8 +125,8 @@ class IListManager(Interface): def delete(mlist): """Remove the mailing list from the database. - :type mlist: `IMailingList` :param mlist: The mailing list to delete. + :type mlist: `IMailingList` """ mailing_lists = Attribute( diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 8519238db..b12d84ec9 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -23,8 +23,9 @@ __metaclass__ = type __all__ = [ 'IAcceptableAlias', 'IAcceptableAliasSet', + 'IListArchiver', + 'IListArchiverSet', 'IMailingList', - 'IArchiverList', 'Personalization', 'ReplyToMunging', ] @@ -55,16 +56,6 @@ class ReplyToMunging(Enum): # An explicit Reply-To header is added explicit_header = 2 -class IArchiverList(Interface): - mailing_list_id = Attribute("""List id""") - archiver_name = Attribute("""Archiver name""") - archiver_enabled = Attribute("""If is enabled.""") - -class IListArchiverSet(Interface): - def getAll(): - """Return dict containing all archivers and their settings.""" - def set(archiver, is_enabled): - """Set archiver for this list.""" class IMailingList(Interface): @@ -802,3 +793,37 @@ class IAcceptableAliasSet(Interface): aliases = Attribute( """An iterator over all the acceptable aliases.""") + + + +class IListArchiver(Interface): + """An archiver for a mailing list. + + The named archiver must be enabled site-wide in order for a mailing list + to be able to enable it. + """ + + mailing_list = Attribute('The associated mailing list.') + + name = Attribute('The name of the archiver.') + + is_enabled = Attribute('Is this archiver enabled for this mailing list?') + + system_archiver = Attribute( + 'The associated system-wide IArchiver instance.') + + +class IListArchiverSet(Interface): + """The set of archivers (enabled or disabled) for a mailing list.""" + + archivers = Attribute( + """An iterator over all the archivers for this mailing list.""") + + def get(archiver_name): + """Return the `IListArchiver` with the given name, if it exists. + + :param archiver_name: The name of the archiver. + :type archiver_name: unicode. + :return: the matching `IListArchiver` or None if the named archiver + does not exist. + """ diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index a0239caa9..e9601e412 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -28,8 +28,8 @@ __all__ = [ import os from storm.locals import ( - And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, ReferenceSet, - Store, TimeDelta, Unicode) + And, Bool, DateTime, Float, Int, Pickle, RawStr, Reference, Store, + TimeDelta, Unicode) from urlparse import urljoin from zope.component import getUtility from zope.event import notify @@ -47,7 +47,7 @@ from mailman.interfaces.digests import DigestFrequency from mailman.interfaces.domain import IDomainManager from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.mailinglist import ( - IAcceptableAlias, IAcceptableAliasSet, IArchiverList, IListArchiverSet, + IAcceptableAlias, IAcceptableAliasSet, IListArchiver, IListArchiverSet, IMailingList, Personalization, ReplyToMunging) from mailman.interfaces.member import ( AlreadySubscribedError, MemberRole, MissingPreferredAddressError, @@ -67,19 +67,6 @@ from mailman.utilities.string import expand SPACE = ' ' UNDERSCORE = '_' -@implementer(IArchiverList) -class ArchiverList(Model): - __storm_primary__ = "mailing_list_id", "archiver_name" - mailing_list_id = Int() - archiver_name = Unicode() - archiver_enabled = Bool() - - def __init__(self, mailing_list_id, archiver_name): - self.mailing_list_id = mailing_list_id - self.archiver_name = archiver_name - self.archiver_enabled = False - - @implementer(IMailingList) @@ -91,7 +78,6 @@ class MailingList(Model): # XXX denotes attributes that should be part of the public interface but # are currently missing. - archivers = ReferenceSet(id, ArchiverList.mailing_list_id) # List identity list_name = Unicode() mail_host = Unicode() @@ -553,39 +539,68 @@ class AcceptableAliasSet: for alias in aliases: yield alias.alias -@implementer(IListArchiverSet) -class ListArchiverSet: - def __init__(self, mailing_list): - self._mailing_list = mailing_list - self.lazyAdd() - def getAll(self): - entries = Store.of(self._mailing_list).find(ArchiverList, ArchiverList.mailing_list_id == self._mailing_list.id) - all_in_config = {archiver.name for archiver in config.archivers} - ret = {} - for entry in entries: - if entry.archiver_name in all_in_config: - ret[entry.archiver_name] = int(entry.archiver_enabled) - return ret + +@implementer(IListArchiver) +class ListArchiver(Model): + """See `IListArchiver`.""" + + id = Int(primary=True) + + mailing_list_id = Int() + mailing_list = Reference(mailing_list_id, MailingList.id) + name = Unicode() + _is_enabled = Bool() + + def __init__(self, mailing_list, archiver_name, system_archiver): + self.mailing_list = mailing_list + self.name = archiver_name + self._is_enabled = system_archiver.is_enabled - def set(self, archiver, is_enabled): - bool_enabled = (int(is_enabled) != 0) - self.get(archiver).set(archiver_enabled=bool_enabled) + @property + def system_archiver(self): + for archiver in config.archivers: + if archiver.name == self.name: + return archiver + return None + + @property + def is_enabled(self): + return self.system_archiver.is_enabled and self._is_enabled - def isEnabled(self, archiverName): - return self.get(archiverName).one().archiver_enabled + @is_enabled.setter + def is_enabled(self, value): + self._is_enabled = value - def get(self, archiverName): - return Store.of(self._mailing_list).find(ArchiverList, - (ArchiverList.mailing_list_id == self._mailing_list.id) & (ArchiverList.archiver_name == archiverName)) - def lazyAdd(self): - names = [] +@implementer(IListArchiverSet) +class ListArchiverSet: + def __init__(self, mailing_list): + self._mailing_list = mailing_list + system_archivers = {} for archiver in config.archivers: - count = self.get(archiver.name).count() - names.append((archiver.name, count)) - if not count: - entry = ArchiverList(self._mailing_list.id, archiver.name) - Store.of(self._mailing_list).add(entry) - Store.of(self._mailing_list).commit() + 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() + 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) + 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() diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py index 3d7f95615..67924d393 100644 --- a/src/mailman/model/tests/test_domain.py +++ b/src/mailman/model/tests/test_domain.py @@ -21,6 +21,8 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'TestDomainLifecycleEvents', + 'TestDomainManager', ] diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py index 152d96b9f..b18c8e5d1 100644 --- a/src/mailman/model/tests/test_listmanager.py +++ b/src/mailman/model/tests/test_listmanager.py @@ -17,10 +17,13 @@ """Test the ListManager.""" -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ + 'TestListCreation', + 'TestListLifecycleEvents', + 'TestListManager', ] diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py new file mode 100644 index 000000000..09c4cb38f --- /dev/null +++ b/src/mailman/model/tests/test_mailinglist.py @@ -0,0 +1,102 @@ +# Copyright (C) 2013 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see . + +"""Test MailingLists and related model objects..""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + 'TestListArchiver', + 'TestDisabledListArchiver', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.interfaces.mailinglist import IListArchiverSet +from mailman.testing.helpers import configuration +from mailman.testing.layers import ConfigLayer + + + +class TestListArchiver(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._set = IListArchiverSet(self._mlist) + + def test_list_archivers(self): + # Find the set of archivers registered for this mailing list. + self.assertEqual( + ['mail-archive', 'mhonarc', 'prototype'], + sorted(archiver.name for archiver in self._set.archivers)) + + def test_get_archiver(self): + # Use .get() to see if a mailing list has an archiver. + archiver = self._set.get('prototype') + self.assertEqual(archiver.name, 'prototype') + self.assertTrue(archiver.is_enabled) + self.assertEqual(archiver.mailing_list, self._mlist) + self.assertEqual(archiver.system_archiver.name, 'prototype') + + def test_get_archiver_no_such(self): + # Using .get() on a non-existing name returns None. + self.assertIsNone(self._set.get('no-such-archiver')) + + def test_site_disabled(self): + # Here the system configuration enables all the archivers in time for + # the archive set to be created with all list archivers enabled. But + # then the site-wide archiver gets disabled, so the list specific + # archiver will also be disabled. + archiver_set = IListArchiverSet(self._mlist) + archiver = archiver_set.get('prototype') + self.assertTrue(archiver.is_enabled) + # Disable the site-wide archiver. + archiver.system_archiver.is_enabled = False + self.assertFalse(archiver.is_enabled) + + + +class TestDisabledListArchiver(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + + @configuration('archiver.prototype', enable='no') + def test_enable_list_archiver(self): + # When the system configuration file disables an archiver site-wide, + # the list-specific mailing list will get initialized as not enabled. + # Create the archiver set on the fly so that it doesn't get + # initialized with a configuration that enables the prototype archiver. + archiver_set = IListArchiverSet(self._mlist) + archiver = archiver_set.get('prototype') + self.assertFalse(archiver.is_enabled) + # Enable both the list archiver and the system archiver. + archiver.is_enabled = True + config.push('enable prototype', """\ + [archiver.prototype] + enable: yes + """) + # Get the IListArchiver again. + archiver = archiver_set.get('prototype') + self.assertTrue(archiver.is_enabled) + config.pop('enable prototype') diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py index eec248ad0..560aafbde 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/configuration.py @@ -26,6 +26,7 @@ __all__ = [ from lazr.config import as_boolean, as_timedelta +from operator import attrgetter from restish import http, resource from mailman.config import config @@ -34,10 +35,10 @@ from mailman.core.errors import ( from mailman.interfaces.action import Action from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.autorespond import ResponseAction -from mailman.interfaces.mailinglist import IAcceptableAliasSet, IListArchiverSet, ReplyToMunging +from mailman.interfaces.mailinglist import ( + IAcceptableAliasSet, IListArchiverSet, ReplyToMunging) from mailman.rest.helpers import GetterSetter, PATCH, etag, no_content from mailman.rest.validator import PatchValidator, Validator, enum_validator -from mailman.model.mailinglist import ListArchiverSet @@ -65,21 +66,24 @@ class AcceptableAliases(GetterSetter): for alias in value: alias_set.add(unicode(alias)) + + class ListArchivers(GetterSetter): + """Resource for list-specific archivers.""" def get(self, mlist, attribute): """Return the mailing list's acceptable aliases.""" assert attribute == 'archivers', ( 'Unexpected attribute: {0}'.format(attribute)) - archivers = ListArchiverSet(mlist) - return archivers.getAll() + archiver_set = IListArchiverSet(mlist) + return sorted(archiver_set.archivers, key=attrgetter('name')) def put(self, mlist, attribute, value): assert attribute == 'archivers', ( 'Unexpected attribute: {0}'.format(attribute)) - archivers = ListArchiverSet(mlist) + archiver_set = IListArchiverSet(mlist) for key, value in value.iteritems(): - archivers.set(key, value) + archivers_set.set(key, value) diff --git a/src/mailman/runners/archive.py b/src/mailman/runners/archive.py index 9eb1dd0a2..907ba5707 100644 --- a/src/mailman/runners/archive.py +++ b/src/mailman/runners/archive.py @@ -36,7 +36,7 @@ from mailman.config import config from mailman.core.runner import Runner from mailman.interfaces.archiver import ClobberDate from mailman.utilities.datetime import RFC822_DATE_FMT, now -from mailman.model.mailinglist import ListArchiverSet +from mailman.interfaces.mailinglist import IListArchiverSet log = logging.getLogger('mailman.error') @@ -91,20 +91,23 @@ class ArchiveRunner(Runner): def _dispose(self, mlist, msg, msgdata): received_time = msgdata.get('received_time', now(strip_tzinfo=False)) - for archiver in config.archivers: - archSet = ListArchiverSet(mlist) - if archSet.isEnabled(archiver.name): - msg_copy = copy.deepcopy(msg) - if _should_clobber(msg, msgdata, archiver.name): - original_date = msg_copy['date'] - del msg_copy['date'] - del msg_copy['x-original-date'] - msg_copy['Date'] = received_time.strftime(RFC822_DATE_FMT) - if original_date: - msg_copy['X-Original-Date'] = original_date - # A problem in one archiver should not prevent other archivers - # from running. - try: - archiver.archive_message(mlist, msg_copy) - except Exception: - log.exception('Broken archiver: %s' % archiver.name) + archiver_set = IListArchiverSet(mlist) + for archiver in archiver_set.archivers: + # The archiver is disabled if either the list-specific or + # site-wide archiver is disabled. + if not archiver.is_enabled: + continue + msg_copy = copy.deepcopy(msg) + if _should_clobber(msg, msgdata, archiver.name): + original_date = msg_copy['date'] + del msg_copy['date'] + del msg_copy['x-original-date'] + msg_copy['Date'] = received_time.strftime(RFC822_DATE_FMT) + if original_date: + msg_copy['X-Original-Date'] = original_date + # A problem in one archiver should not prevent other archivers + # from running. + try: + archiver.system_archiver.archive_message(mlist, msg_copy) + except Exception: + log.exception('Broken archiver: %s' % archiver.name) diff --git a/src/mailman/runners/tests/test_archiver.py b/src/mailman/runners/tests/test_archiver.py index 80a676dfd..f0ac00458 100644 --- a/src/mailman/runners/tests/test_archiver.py +++ b/src/mailman/runners/tests/test_archiver.py @@ -34,6 +34,7 @@ from zope.interface import implementer from mailman.app.lifecycle import create_list from mailman.config import config from mailman.interfaces.archiver import IArchiver +from mailman.interfaces.mailinglist import IListArchiverSet from mailman.runners.archive import ArchiveRunner from mailman.testing.helpers import ( configuration, @@ -99,6 +100,7 @@ X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB First post! """) self._runner = make_testable_runner(ArchiveRunner) + IListArchiverSet(self._mlist).get('dummy').is_enabled = True def tearDown(self): config.pop('dummy') @@ -237,3 +239,15 @@ First post! self.assertEqual(archived['message-id'], '') self.assertEqual(archived['date'], 'Mon, 01 Aug 2005 07:49:23 +0000') self.assertEqual(archived['x-original-date'], None) + + @configuration('archiver.dummy', enable='yes') + def test_disable_all_list_archivers(self): + # Let's disable all the archivers for the mailing list, but not the + # global archivers. No messages will get archived. + for archiver in IListArchiverSet(self._mlist).archivers: + archiver.is_enabled = False + self._archiveq.enqueue( + self._msg, {}, + listname=self._mlist.fqdn_listname) + self._runner.run() + self.assertEqual(os.listdir(config.MESSAGES_DIR), []) -- cgit v1.3.1 From 177d3f81f4c786ad51083dfce6c4a5fd127693bd Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 26 Nov 2013 17:07:11 -0500 Subject: More fixes. --- src/mailman/model/tests/test_mailinglist.py | 6 +++++- src/mailman/runners/tests/test_archiver.py | 1 + src/mailman/testing/helpers.py | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py index 09c4cb38f..b2dbbf1ca 100644 --- a/src/mailman/model/tests/test_mailinglist.py +++ b/src/mailman/model/tests/test_mailinglist.py @@ -70,8 +70,12 @@ class TestListArchiver(unittest.TestCase): archiver = archiver_set.get('prototype') self.assertTrue(archiver.is_enabled) # Disable the site-wide archiver. - archiver.system_archiver.is_enabled = False + config.push('enable prototype', """\ + [archiver.prototype] + enable: no + """) self.assertFalse(archiver.is_enabled) + config.pop('enable prototype') diff --git a/src/mailman/runners/tests/test_archiver.py b/src/mailman/runners/tests/test_archiver.py index f0ac00458..f7087f28f 100644 --- a/src/mailman/runners/tests/test_archiver.py +++ b/src/mailman/runners/tests/test_archiver.py @@ -246,6 +246,7 @@ First post! # global archivers. No messages will get archived. for archiver in IListArchiverSet(self._mlist).archivers: archiver.is_enabled = False + config.db.store.commit() self._archiveq.enqueue( self._msg, {}, listname=self._mlist.fqdn_listname) diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py index 8201f952a..9f9e28dc6 100644 --- a/src/mailman/testing/helpers.py +++ b/src/mailman/testing/helpers.py @@ -478,6 +478,10 @@ def reset_the_world(): with transaction(): for message in message_store.messages: message_store.delete_message(message['message-id']) + # Delete any other residual messages. + for dirpath, dirnames, filenames in os.walk(config.MESSAGES_DIR): + for filename in filenames: + os.remove(os.path.join(dirpath, filename)) # Reset the global style manager. getUtility(IStyleManager).populate() # Remove all dynamic header-match rules. -- cgit v1.3.1 From fc347a34a65ebd0a249da52079d7aa60621b3eb2 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 27 Nov 2013 13:50:20 -0500 Subject: Add lists//archivers as the resource end-point. We won't set archivers in the list's configuration. Add doctests and unittests for archivers. --- src/mailman/rest/configuration.py | 33 +------------- src/mailman/rest/docs/lists.rst | 58 +++++++++++++++++++++++++ src/mailman/rest/lists.py | 69 +++++++++++++++++++++++++++++- src/mailman/rest/tests/test_lists.py | 83 +++++++++++++++++++++++++++--------- 4 files changed, 190 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py index 560aafbde..c726d8a81 100644 --- a/src/mailman/rest/configuration.py +++ b/src/mailman/rest/configuration.py @@ -26,7 +26,6 @@ __all__ = [ from lazr.config import as_boolean, as_timedelta -from operator import attrgetter from restish import http, resource from mailman.config import config @@ -35,8 +34,7 @@ from mailman.core.errors import ( from mailman.interfaces.action import Action from mailman.interfaces.archiver import ArchivePolicy from mailman.interfaces.autorespond import ResponseAction -from mailman.interfaces.mailinglist import ( - IAcceptableAliasSet, IListArchiverSet, ReplyToMunging) +from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging from mailman.rest.helpers import GetterSetter, PATCH, etag, no_content from mailman.rest.validator import PatchValidator, Validator, enum_validator @@ -67,25 +65,6 @@ class AcceptableAliases(GetterSetter): alias_set.add(unicode(alias)) - -class ListArchivers(GetterSetter): - """Resource for list-specific archivers.""" - - def get(self, mlist, attribute): - """Return the mailing list's acceptable aliases.""" - assert attribute == 'archivers', ( - 'Unexpected attribute: {0}'.format(attribute)) - archiver_set = IListArchiverSet(mlist) - return sorted(archiver_set.archivers, key=attrgetter('name')) - - def put(self, mlist, attribute, value): - assert attribute == 'archivers', ( - 'Unexpected attribute: {0}'.format(attribute)) - archiver_set = IListArchiverSet(mlist) - for key, value in value.iteritems(): - archivers_set.set(key, value) - - # Additional validators for converting from web request strings to internal # data types. See below for details. @@ -101,15 +80,6 @@ def list_of_unicode(values): """Turn a list of things into a list of unicodes.""" return [unicode(value) for value in values] -def list_of_pairs_to_dict(pairs): - dict = {} - # If pairs has only one element then it is not a list but a string. - if not isinstance(pairs, list): - pairs = [pairs] - for key_value in pairs: - parts = key_value.split('|') - dict[parts[0]] = parts[1] - return dict # This is the list of IMailingList attributes that are exposed through the @@ -128,7 +98,6 @@ def list_of_pairs_to_dict(pairs): ATTRIBUTES = dict( acceptable_aliases=AcceptableAliases(list_of_unicode), - archivers=ListArchivers(list_of_pairs_to_dict), admin_immed_notify=GetterSetter(as_boolean), admin_notify_mchanges=GetterSetter(as_boolean), administrivia=GetterSetter(as_boolean), diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst index 295e8c0b7..27503c1c1 100644 --- a/src/mailman/rest/docs/lists.rst +++ b/src/mailman/rest/docs/lists.rst @@ -230,3 +230,61 @@ The mailing list does not exist. >>> print list_manager.get('ant@example.com') None + + +Managing mailing list archivers +=============================== + +The Mailman system has some site-wide enabled archivers, and each mailing list +can enable or disable these archivers individually. This gives list owners +control over where traffic to their list is archived. You can see which +archivers are available, and whether they are enabled for this mailing list. +:: + + >>> mlist = create_list('dog@example.com') + >>> transaction.commit() + + >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers') + http_etag: "..." + mail-archive: True + mhonarc: True + prototype: True + +You can set all the archiver states by putting new state flags on the +resource. +:: + + >>> dump_json( + ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', { + ... 'mail-archive': False, + ... 'mhonarc': True, + ... 'prototype': False, + ... }, method='PUT') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers') + http_etag: "..." + mail-archive: False + mhonarc: True + prototype: False + +You can change the state of a subset of the list archivers. +:: + + >>> dump_json( + ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', { + ... 'mhonarc': False, + ... }, method='PATCH') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers') + http_etag: "..." + mail-archive: False + mhonarc: False + prototype: False diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 32e22a76b..b8e754647 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -23,11 +23,13 @@ __metaclass__ = type __all__ = [ 'AList', 'AllLists', + 'ListArchivers', 'ListConfiguration', 'ListsForDomain', ] +from lazr.config import as_boolean from operator import attrgetter from restish import http, resource from zope.component import getUtility @@ -36,11 +38,13 @@ from mailman.app.lifecycle import create_list, remove_list from mailman.interfaces.domain import BadDomainSpecificationError from mailman.interfaces.listmanager import ( IListManager, ListAlreadyExistsError) +from mailman.interfaces.mailinglist import IListArchiverSet from mailman.interfaces.member import MemberRole from mailman.interfaces.subscriptions import ISubscriptionService from mailman.rest.configuration import ListConfiguration from mailman.rest.helpers import ( - CollectionMixin, etag, no_content, paginate, path_to, restish_matcher) + CollectionMixin, GetterSetter, PATCH, etag, no_content, paginate, path_to, + restish_matcher) from mailman.rest.members import AMember, MemberCollection from mailman.rest.moderation import HeldMessages, SubscriptionRequests from mailman.rest.validator import Validator @@ -189,6 +193,13 @@ class AList(_ListBase): return http.not_found() return SubscriptionRequests(self._mlist) + @resource.child() + def archivers(self, request, segments): + """Return a representation of mailing list archivers.""" + if self._mlist is None: + return http.not_found() + return ListArchivers(self._mlist) + class AllLists(_ListBase): @@ -256,3 +267,59 @@ class ListsForDomain(_ListBase): def _get_collection(self, request): """See `CollectionMixin`.""" return list(self._domain.mailing_lists) + + + +class ArchiverGetterSetter(GetterSetter): + """Resource for updating archiver statuses.""" + + def __init__(self, mlist): + super(ArchiverGetterSetter, self).__init__() + self._archiver_set = IListArchiverSet(mlist) + + def put(self, mlist, attribute, value): + # attribute will contain the (bytes) name of the archiver that is + # getting a new status. value will be the representation of the new + # boolean status. + archiver = self._archiver_set.get(attribute.decode('utf-8')) + if archiver is None: + raise ValueError('No such archiver: {}'.format(attribute)) + archiver.is_enabled = as_boolean(value) + + +class ListArchivers(resource.Resource): + """The archivers for a list, with their enabled flags.""" + + def __init__(self, mlist): + self._mlist = mlist + + @resource.GET() + def statuses(self, request): + """Get all the archiver statuses.""" + archiver_set = IListArchiverSet(self._mlist) + resource = {archiver.name: archiver.is_enabled + for archiver in archiver_set.archivers} + return http.ok([], etag(resource)) + + def patch_put(self, request, is_optional): + archiver_set = IListArchiverSet(self._mlist) + kws = {archiver.name: ArchiverGetterSetter(self._mlist) + for archiver in archiver_set.archivers} + if is_optional: + # For a PUT, all attributes are optional. + kws['_optional'] = kws.keys() + try: + Validator(**kws).update(self._mlist, request) + except ValueError as error: + return http.bad_request([], str(error)) + return no_content() + + @resource.PUT() + def put_statuses(self, request): + """Update all the archiver statuses.""" + return self.patch_put(request, is_optional=False) + + @PATCH() + def patch_statuses(self, request): + """Patch some archiver statueses.""" + return self.patch_put(request, is_optional=True) diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py index 1c11865e6..77b85895b 100644 --- a/src/mailman/rest/tests/test_lists.py +++ b/src/mailman/rest/tests/test_lists.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ + 'TestListArchivers', 'TestLists', 'TestListsMissing', ] @@ -28,16 +29,12 @@ __all__ = [ import unittest -from zope.component import getUtility from urllib2 import HTTPError from zope.component import getUtility from mailman.app.lifecycle import create_list -from mailman.config import config from mailman.database.transaction import transaction from mailman.interfaces.usermanager import IUserManager -from mailman.interfaces.listmanager import IListManager -from mailman.model.mailinglist import ListArchiverSet from mailman.testing.helpers import call_api from mailman.testing.layers import RESTLayer @@ -164,24 +161,70 @@ class TestLists(unittest.TestCase): method='DELETE') self.assertEqual(cm.exception.code, 404) - def test_prototype_in_list_archivers(self): - resource, response = call_api( - 'http://localhost:9001/3.0/lists/test@example.com/config') - self.assertEqual(response.status, 200) - self.assertEqual(resource['archivers']['prototype'], 0) - def test_lazy_add_archivers(self): - call_api('http://localhost:9001/3.0/lists', { - 'fqdn_listname': 'new_list@example.com', - }) + +class TestListArchivers(unittest.TestCase): + """Test corner cases for list archivers.""" + + layer = RESTLayer + + def setUp(self): + with transaction(): + self._mlist = create_list('ant@example.com') + + def test_archiver_statuses(self): resource, response = call_api( - 'http://localhost:9001/3.0/lists/new_list@example.com/config') + 'http://localhost:9001/3.0/lists/ant.example.com/archivers') self.assertEqual(response.status, 200) - self.assertEqual(resource['archivers']['prototype'], 0) + # Remove the variable data. + resource.pop('http_etag') + self.assertEqual(resource, { + 'mail-archive': True, + 'mhonarc': True, + 'prototype': True, + }) + + def test_archiver_statuses_on_missing_lists(self): + # You cannot get the archiver statuses on a list that doesn't exist. + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/lists/bee.example.com/archivers') + self.assertEqual(cm.exception.code, 404) - def test_set_archiver_enabled(self): - mlist = getUtility(IListManager).create('newest_list@example.com') - lset = ListArchiverSet(mlist) - lset.set('prototype', 1) - self.assertEqual(lset.isEnabled('prototype'), 1) + def test_patch_status_on_bogus_archiver(self): + # You cannot set the status on an archiver the list doesn't know about. + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/lists/ant.example.com/archivers', { + 'bogus-archiver': True, + }, + method='PATCH') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, + 'Unexpected parameters: bogus-archiver') + def test_put_incomplete_statuses(self): + # PUT requires the full resource representation. This one forgets to + # specify the prototype and mhonarc archiver. + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/lists/ant.example.com/archivers', { + 'mail-archive': True, + }, + method='PUT') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, + 'Missing parameters: mhonarc, prototype') + + def test_patch_bogus_status(self): + # Archiver statuses must be interpretable as booleans. + with self.assertRaises(HTTPError) as cm: + call_api( + 'http://localhost:9001/3.0/lists/ant.example.com/archivers', { + 'mail-archive': 'sure', + 'mhonarc': False, + 'prototype': 'no' + }, + method='PATCH') + self.assertEqual(cm.exception.code, 400) + self.assertEqual(cm.exception.reason, 'Invalid boolean value: sure') -- cgit v1.3.1 From b3ce2a4f6106fa4b2d014ab921f9b6a25b067de3 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 27 Nov 2013 14:40:14 -0500 Subject: Archived-At header calculation must observe list archivers. --- src/mailman/handlers/rfc_2369.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/mailman/handlers/rfc_2369.py b/src/mailman/handlers/rfc_2369.py index d203f747a..a0b50c75f 100644 --- a/src/mailman/handlers/rfc_2369.py +++ b/src/mailman/handlers/rfc_2369.py @@ -32,6 +32,7 @@ from mailman.config import config from mailman.core.i18n import _ from mailman.handlers.cook_headers import uheader from mailman.interfaces.archiver import ArchivePolicy +from mailman.interfaces.mailinglist import IListArchiverSet from mailman.interfaces.handler import IHandler @@ -84,10 +85,13 @@ def process(mlist, msg, msgdata): headers['List-Post'] = list_post # Add RFC 2369 and 5064 archiving headers, if archiving is enabled. if mlist.archive_policy is not ArchivePolicy.never: - for archiver in config.archivers: + archiver_set = IListArchiverSet(mlist) + for archiver in archiver_set.archivers: + if not archiver.is_enabled: + continue headers['List-Archive'] = '<{0}>'.format( - archiver.list_url(mlist)) - permalink = archiver.permalink(mlist, msg) + archiver.system_archiver.list_url(mlist)) + permalink = archiver.system_archiver.permalink(mlist, msg) if permalink is not None: headers['Archived-At'] = permalink # XXX RFC 2369 also defines a List-Owner header which we are not currently -- cgit v1.3.1