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