summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/core/initialize.py3
-rw-r--r--src/mailman/database/base.py15
-rw-r--r--src/mailman/database/docs/migration.rst41
-rw-r--r--src/mailman/database/schema/mm_20120407000000.py70
-rw-r--r--src/mailman/database/schema/sqlite.sql3
-rw-r--r--src/mailman/database/schema/sqlite_20120407000000_01.sql244
-rw-r--r--src/mailman/database/tests/__init__.py0
-rw-r--r--src/mailman/database/tests/data/__init__.py0
-rw-r--r--src/mailman/database/tests/data/mailman_01.dbbin0 -> 48128 bytes
-rw-r--r--src/mailman/database/tests/test_migrations.py152
-rw-r--r--src/mailman/interfaces/archiver.py8
-rw-r--r--src/mailman/interfaces/mailinglist.py10
-rw-r--r--src/mailman/interfaces/nntp.py4
-rw-r--r--src/mailman/model/mailinglist.py12
-rw-r--r--src/mailman/rules/docs/news-moderation.rst6
-rw-r--r--src/mailman/rules/news_moderation.py4
-rw-r--r--src/mailman/runners/nntp.py10
-rw-r--r--src/mailman/runners/tests/test_nntp.py18
-rw-r--r--src/mailman/styles/default.py6
-rw-r--r--src/mailman/testing/helpers.py30
-rw-r--r--src/mailman/tests/test_documentation.py20
-rw-r--r--src/mailman/utilities/importer.py4
22 files changed, 599 insertions, 61 deletions
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
index 389a45f3b..5659661a7 100644
--- a/src/mailman/core/initialize.py
+++ b/src/mailman/core/initialize.py
@@ -124,6 +124,7 @@ def initialize_1(config_path=None):
def initialize_2(debug=False, propagate_logs=None):
"""Second initialization step.
+ * Database
* Logging
* Pre-hook
* Rules
@@ -148,6 +149,8 @@ def initialize_2(debug=False, propagate_logs=None):
database = call_name(database_class)
verifyObject(IDatabase, database)
database.initialize(debug)
+ database.load_migrations()
+ database.commit()
config.db = database
# Initialize the rules and chains. Do the imports here so as to avoid
# circular imports.
diff --git a/src/mailman/database/base.py b/src/mailman/database/base.py
index 1595007f1..55dee2068 100644
--- a/src/mailman/database/base.py
+++ b/src/mailman/database/base.py
@@ -142,11 +142,15 @@ class StormBaseDatabase:
database.DEBUG = (as_boolean(config.database.debug)
if debug is None else debug)
self.store = store
- self.load_migrations()
store.commit()
- def load_migrations(self):
- """Load all not-yet loaded migrations."""
+ def load_migrations(self, until=None):
+ """Load schema migrations.
+
+ :param until: Load only the migrations up to the specified timestamp.
+ With default value of None, load all migrations.
+ :type until: string
+ """
migrations_path = config.database.migrations_path
if '.' in migrations_path:
parent, dot, child = migrations_path.rpartition('.')
@@ -169,9 +173,12 @@ class StormBaseDatabase:
if len(parts) < 2:
continue
version = parts[1]
- if version in versions:
+ if len(version.strip()) == 0 or version in versions:
# This one is already loaded.
continue
+ if until is not None and version > until:
+ # We're done.
+ break
module_path = migrations_path + '.' + module_fn
__import__(module_path)
upgrade = getattr(sys.modules[module_path], 'upgrade', None)
diff --git a/src/mailman/database/docs/migration.rst b/src/mailman/database/docs/migration.rst
index 9897b1ef2..999700ca7 100644
--- a/src/mailman/database/docs/migration.rst
+++ b/src/mailman/database/docs/migration.rst
@@ -55,6 +55,14 @@ specified in the configuration file.
... migrations_path: migrations
... """)
+.. Clean this up at the end of the doctest.
+ >>> def cleanup():
+ ... import shutil
+ ... from mailman.config import config
+ ... config.pop('migrations')
+ ... shutil.rmtree(tempdir)
+ >>> cleanups.append(cleanup)
+
Here is an example migrations module. The key part of this interface is the
``upgrade()`` method, which takes four arguments:
@@ -138,9 +146,32 @@ We do not get an 802 marker because the migration has already been loaded.
00000000000801
20120211000000
-.. Clean up the temporary directory::
- >>> config.pop('migrations')
- >>> sys.path.remove(tempdir)
- >>> import shutil
- >>> shutil.rmtree(tempdir)
+Partial upgrades
+================
+
+It's possible (mostly for testing purposes) to only do a partial upgrade, by
+providing a timestamp to `load_migrations()`. To demonstrate this, we add two
+additional migrations, intended to be applied in sequential order.
+
+ >>> from shutil import copyfile
+ >>> from mailman.testing.helpers import chdir
+ >>> with chdir(path):
+ ... copyfile('mm_20120211000000.py', 'mm_20120211000002.py')
+ ... copyfile('mm_20120211000000.py', 'mm_20120211000003.py')
+ ... copyfile('mm_20120211000000.py', 'mm_20120211000004.py')
+
+Now, only migrate to the ...03 timestamp.
+
+ >>> config.db.load_migrations('20120211000003')
+
+You'll notice that the ...04 version is not present.
+
+ >>> results = config.db.store.find(Version, component='schema')
+ >>> for result in sorted(result.version for result in results):
+ ... print result
+ 00000000000000
+ 20120211000000
+ 20120211000001
+ 20120211000002
+ 20120211000003
diff --git a/src/mailman/database/schema/mm_20120407000000.py b/src/mailman/database/schema/mm_20120407000000.py
new file mode 100644
index 000000000..555d35ea2
--- /dev/null
+++ b/src/mailman/database/schema/mm_20120407000000.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
+
+"""3.0b1 -> 3.0b2 schema migrations."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]
+
+
+from mailman.interfaces.archiver import ArchivePolicy
+from mailman.interfaces.database import DatabaseError
+
+
+
+def upgrade(database, store, version, module_path):
+ if database.TAG == 'sqlite':
+ upgrade_sqlite(database, store, version, module_path)
+ else:
+ # XXX 2012-04-07 BAW: Implement PostgreSQL migration.
+ raise DatabaseError('Database {0} migration not support: {1}'.format(
+ database.TAG, version))
+
+
+
+def upgrade_sqlite(database, store, version, module_path):
+ # Load the first part of the migration. This creates a temporary table to
+ # hold the new mailinglist table columns. The problem is that some of the
+ # changes must be performed in Python, so after the first part is loaded,
+ # we do the Python changes, drop the old mailing list table, and then
+ # rename the temporary table to its place.
+ database.load_schema(
+ store, version, 'sqlite_{0}_01.sql'.format(version), module_path)
+ results = store.execute(
+ 'select id, news_prefix_subject_too, news_moderation, '
+ 'archive, archive_private from mailinglist;')
+ for value in results:
+ id, news_prefix, news_moderation, archive, archive_private = value
+ # Figure out what the new archive_policy column value should be.
+ if archive == 0:
+ archive_policy = int(ArchivePolicy.never)
+ elif archive_private == 1:
+ archive_policy = int(ArchivePolicy.private)
+ else:
+ archive_policy = int(ArchivePolicy.public)
+ store.execute(
+ 'update ml_backup set '
+ ' newsgroup_moderation = {0}, '
+ ' nntp_prefix_subject_too = {1}, '
+ ' archive_policy = {2} '
+ 'where id = {2};'.format(news_moderation, news_prefix,
+ archive_policy, id))
+ store.execute('drop table mailinglist;')
+ store.execute('alter table ml_backup rename to mailinglist;')
diff --git a/src/mailman/database/schema/sqlite.sql b/src/mailman/database/schema/sqlite.sql
index e6211bf53..e2b2d3814 100644
--- a/src/mailman/database/schema/sqlite.sql
+++ b/src/mailman/database/schema/sqlite.sql
@@ -1,3 +1,6 @@
+-- THIS FILE HAS BEEN FROZEN AS OF 3.0b1
+-- SEE THE SCHEMA MIGRATIONS FOR DIFFERENCES.
+
PRAGMA foreign_keys = ON;
CREATE TABLE _request (
diff --git a/src/mailman/database/schema/sqlite_20120407000000_01.sql b/src/mailman/database/schema/sqlite_20120407000000_01.sql
new file mode 100644
index 000000000..58201fb9b
--- /dev/null
+++ b/src/mailman/database/schema/sqlite_20120407000000_01.sql
@@ -0,0 +1,244 @@
+-- THIS FILE CONTAINS THE SQLITE3 SCHEMA MIGRATION FROM
+-- 3.0b1 TO 3.0b2
+--
+-- AFTER 3.0b2 IS RELEASED YOU MAY NOT EDIT THIS FILE.
+
+-- For SQLite3 migration strategy, see
+-- http://sqlite.org/faq.html#q11
+
+-- This is the base mailinglist table but with these changes:
+-- REM archive
+-- REM archive_private
+-- REM archive_volume_frequency
+-- REM news_moderation
+-- REM news_prefix_subject_too
+-- REM nntp_host
+--
+-- THESE COLUMNS ARE ADDED BY THE PYTHON MIGRATION LAYER:
+-- ADD archive_policy
+-- ADD newsgroup_moderation
+-- ADD nntp_prefix_subject_too
+-- LP: #971013
+
+CREATE TABLE ml_backup(
+ id INTEGER NOT NULL,
+ -- List identity
+ list_name TEXT,
+ mail_host TEXT,
+ include_list_post_header BOOLEAN,
+ include_rfc2369_headers BOOLEAN,
+ -- Attributes not directly modifiable via the web u/i
+ created_at TIMESTAMP,
+ admin_member_chunksize INTEGER,
+ next_request_id INTEGER,
+ next_digest_number INTEGER,
+ digest_last_sent_at TIMESTAMP,
+ volume INTEGER,
+ last_post_at TIMESTAMP,
+ accept_these_nonmembers BLOB,
+ acceptable_aliases_id INTEGER,
+ admin_immed_notify BOOLEAN,
+ admin_notify_mchanges BOOLEAN,
+ administrivia BOOLEAN,
+ advertised BOOLEAN,
+ anonymous_list BOOLEAN,
+ -- Automatic responses.
+ autorespond_owner INTEGER,
+ autoresponse_owner_text TEXT,
+ autorespond_postings INTEGER,
+ autoresponse_postings_text TEXT,
+ autorespond_requests INTEGER,
+ autoresponse_request_text TEXT,
+ autoresponse_grace_period TEXT,
+ -- Bounces.
+ forward_unrecognized_bounces_to INTEGER,
+ process_bounces BOOLEAN,
+ bounce_info_stale_after TEXT,
+ bounce_matching_headers TEXT,
+ bounce_notify_owner_on_disable BOOLEAN,
+ bounce_notify_owner_on_removal BOOLEAN,
+ bounce_score_threshold INTEGER,
+ bounce_you_are_disabled_warnings INTEGER,
+ bounce_you_are_disabled_warnings_interval TEXT,
+ -- Content filtering.
+ filter_action INTEGER,
+ filter_content BOOLEAN,
+ collapse_alternatives BOOLEAN,
+ convert_html_to_plaintext BOOLEAN,
+ default_member_action INTEGER,
+ default_nonmember_action INTEGER,
+ description TEXT,
+ digest_footer_uri TEXT,
+ digest_header_uri TEXT,
+ digest_is_default BOOLEAN,
+ digest_send_periodic BOOLEAN,
+ digest_size_threshold FLOAT,
+ digest_volume_frequency INTEGER,
+ digestable BOOLEAN,
+ discard_these_nonmembers BLOB,
+ emergency BOOLEAN,
+ encode_ascii_prefixes BOOLEAN,
+ first_strip_reply_to BOOLEAN,
+ footer_uri TEXT,
+ forward_auto_discards BOOLEAN,
+ gateway_to_mail BOOLEAN,
+ gateway_to_news BOOLEAN,
+ generic_nonmember_action INTEGER,
+ goodbye_message_uri TEXT,
+ header_matches BLOB,
+ header_uri TEXT,
+ hold_these_nonmembers BLOB,
+ info TEXT,
+ linked_newsgroup TEXT,
+ max_days_to_hold INTEGER,
+ max_message_size INTEGER,
+ max_num_recipients INTEGER,
+ member_moderation_notice TEXT,
+ mime_is_default_digest BOOLEAN,
+ moderator_password TEXT,
+ new_member_options INTEGER,
+ nondigestable BOOLEAN,
+ nonmember_rejection_notice TEXT,
+ obscure_addresses BOOLEAN,
+ owner_chain TEXT,
+ owner_pipeline TEXT,
+ personalize INTEGER,
+ post_id INTEGER,
+ posting_chain TEXT,
+ posting_pipeline TEXT,
+ preferred_language TEXT,
+ private_roster BOOLEAN,
+ display_name TEXT,
+ reject_these_nonmembers BLOB,
+ reply_goes_to_list INTEGER,
+ reply_to_address TEXT,
+ require_explicit_destination BOOLEAN,
+ respond_to_post_requests BOOLEAN,
+ scrub_nondigest BOOLEAN,
+ send_goodbye_message BOOLEAN,
+ send_reminders BOOLEAN,
+ send_welcome_message BOOLEAN,
+ subject_prefix TEXT,
+ subscribe_auto_approval BLOB,
+ subscribe_policy INTEGER,
+ topics BLOB,
+ topics_bodylines_limit INTEGER,
+ topics_enabled BOOLEAN,
+ unsubscribe_policy INTEGER,
+ welcome_message_uri TEXT,
+ PRIMARY KEY (id)
+ );
+
+
+INSERT INTO ml_backup SELECT
+ id,
+ -- List identity
+ list_name,
+ mail_host,
+ include_list_post_header,
+ include_rfc2369_headers,
+ -- Attributes not directly modifiable via the web u/i
+ created_at,
+ admin_member_chunksize,
+ next_request_id,
+ next_digest_number,
+ digest_last_sent_at,
+ volume,
+ last_post_at,
+ accept_these_nonmembers,
+ acceptable_aliases_id,
+ admin_immed_notify,
+ admin_notify_mchanges,
+ administrivia,
+ advertised,
+ anonymous_list,
+ -- Automatic responses.
+ autorespond_owner,
+ autoresponse_owner_text,
+ autorespond_postings,
+ autoresponse_postings_text,
+ autorespond_requests,
+ autoresponse_request_text,
+ autoresponse_grace_period,
+ -- Bounces.
+ forward_unrecognized_bounces_to,
+ process_bounces,
+ bounce_info_stale_after,
+ bounce_matching_headers,
+ bounce_notify_owner_on_disable,
+ bounce_notify_owner_on_removal,
+ bounce_score_threshold,
+ bounce_you_are_disabled_warnings,
+ bounce_you_are_disabled_warnings_interval,
+ -- Content filtering.
+ filter_action,
+ filter_content,
+ collapse_alternatives,
+ convert_html_to_plaintext,
+ default_member_action,
+ default_nonmember_action,
+ description,
+ digest_footer_uri,
+ digest_header_uri,
+ digest_is_default,
+ digest_send_periodic,
+ digest_size_threshold,
+ digest_volume_frequency,
+ digestable,
+ discard_these_nonmembers,
+ emergency,
+ encode_ascii_prefixes,
+ first_strip_reply_to,
+ footer_uri,
+ forward_auto_discards,
+ gateway_to_mail,
+ gateway_to_news,
+ generic_nonmember_action,
+ goodbye_message_uri,
+ header_matches,
+ header_uri,
+ hold_these_nonmembers,
+ info,
+ linked_newsgroup,
+ max_days_to_hold,
+ max_message_size,
+ max_num_recipients,
+ member_moderation_notice,
+ mime_is_default_digest,
+ moderator_password,
+ new_member_options,
+ nondigestable,
+ nonmember_rejection_notice,
+ obscure_addresses,
+ owner_chain,
+ owner_pipeline,
+ personalize,
+ post_id,
+ posting_chain,
+ posting_pipeline,
+ preferred_language,
+ private_roster,
+ display_name,
+ reject_these_nonmembers,
+ reply_goes_to_list,
+ reply_to_address,
+ require_explicit_destination,
+ respond_to_post_requests,
+ scrub_nondigest,
+ send_goodbye_message,
+ send_reminders,
+ send_welcome_message,
+ subject_prefix,
+ subscribe_auto_approval,
+ subscribe_policy,
+ topics,
+ topics_bodylines_limit,
+ topics_enabled,
+ unsubscribe_policy,
+ welcome_message_uri
+ FROM mailinglist;
+
+-- Add the new columns. They'll get inserted at the Python layer.
+ALTER TABLE ml_backup ADD COLUMN archive_policy INTEGER;
+ALTER TABLE ml_backup ADD COLUMN nntp_prefix_subject_too INTEGER;
+ALTER TABLE ml_backup ADD COLUMN newsgroup_moderation INTEGER;
diff --git a/src/mailman/database/tests/__init__.py b/src/mailman/database/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/database/tests/__init__.py
diff --git a/src/mailman/database/tests/data/__init__.py b/src/mailman/database/tests/data/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/database/tests/data/__init__.py
diff --git a/src/mailman/database/tests/data/mailman_01.db b/src/mailman/database/tests/data/mailman_01.db
new file mode 100644
index 000000000..1ff8d8343
--- /dev/null
+++ b/src/mailman/database/tests/data/mailman_01.db
Binary files differ
diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py
new file mode 100644
index 000000000..8b4dd1f43
--- /dev/null
+++ b/src/mailman/database/tests/test_migrations.py
@@ -0,0 +1,152 @@
+# Copyright (C) 2012 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 <http://www.gnu.org/licenses/>.
+
+"""Test schema migrations."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestMigration20120407',
+ ]
+
+
+import os
+import shutil
+import sqlite3
+import tempfile
+import unittest
+
+from pkg_resources import resource_filename
+from zope.component import getUtility
+
+from mailman.config import config
+from mailman.interfaces.domain import IDomainManager
+from mailman.interfaces.listmanager import IListManager
+from mailman.interfaces.mailinglist import IAcceptableAliasSet
+from mailman.testing.helpers import configuration
+from mailman.testing.layers import ConfigLayer
+from mailman.utilities.modules import call_name
+
+
+
+class TestMigration20120407(unittest.TestCase):
+ """Test the dated migration (LP: #971013)
+
+ Circa: 3.0b1 -> 3.0b2
+
+ table mailinglist:
+ * news_moderation -> newsgroup_moderation
+ * news_prefix_subject_too -> nntp_prefix_subject_too
+ * ADD archive_policy
+ * REMOVE archive
+ * REMOVE archive_private
+ * REMOVE archive_volume_frequency
+ * REMOVE nntp_host
+ """
+
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._tempdir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self._tempdir)
+
+ def test_sqlite_base(self):
+ # Test that before the migration, the old table columns are present
+ # and the new database columns are not.
+ url = 'sqlite:///' + os.path.join(self._tempdir, 'mailman.db')
+ database_class = config.database['class']
+ database = call_name(database_class)
+ with configuration('database', url=url):
+ database.initialize()
+ # Load all the database SQL to just before ours.
+ database.load_migrations('20120406999999')
+ # Verify that the database has not yet been migrated.
+ for missing in ('archive_policy',
+ 'nntp_prefix_subject_too'):
+ self.assertRaises(sqlite3.OperationalError,
+ database.store.execute,
+ 'select {0} from mailinglist;'.format(missing))
+ for present in ('archive',
+ 'archive_private',
+ 'archive_volume_frequency',
+ 'news_moderation',
+ 'news_prefix_subject_too',
+ 'nntp_host'):
+ # This should not produce an exception. Is there some better test
+ # that we can perform?
+ database.store.execute(
+ 'select {0} from mailinglist;'.format(present))
+
+ def test_sqlite_migration(self):
+ # Test that after the migration, the old table columns are missing
+ # and the new database columns are present.
+ url = 'sqlite:///' + os.path.join(self._tempdir, 'mailman.db')
+ database_class = config.database['class']
+ database = call_name(database_class)
+ with configuration('database', url=url):
+ database.initialize()
+ # Load all the database SQL to just before ours.
+ database.load_migrations('20120406999999')
+ # Load all migrations, up to and including this one.
+ database.load_migrations('20120407000000')
+ # Verify that the database has been migrated.
+ for present in ('archive_policy',
+ 'nntp_prefix_subject_too'):
+ # This should not produce an exception. Is there some better test
+ # that we can perform?
+ database.store.execute(
+ 'select {0} from mailinglist;'.format(present))
+ for missing in ('archive',
+ 'archive_private',
+ 'archive_volume_frequency',
+ 'news_moderation',
+ 'news_prefix_subject_too',
+ 'nntp_host'):
+ self.assertRaises(sqlite3.OperationalError,
+ database.store.execute,
+ 'select {0} from mailinglist;'.format(missing))
+
+ def test_data_after_migration(self):
+ # Ensure that the existing data and foreign key references are
+ # preserved across a migration. Unfortunately, this requires sample
+ # data, which kind of sucks.
+ dst = os.path.join(self._tempdir, 'mailman.db')
+ src = resource_filename('mailman.database.tests.data', 'mailman_01.db')
+ shutil.copyfile(src, dst)
+ url = 'sqlite:///' + dst
+ database_class = config.database['class']
+ database = call_name(database_class)
+ with configuration('database', url=url):
+ # Initialize the database and perform the migrations.
+ database.initialize()
+ database.load_migrations('20120407000000')
+ # Check that the domains survived the migration. This table was
+ # not touched so it should be fine.
+ domains = list(getUtility(IDomainManager))
+ self.assertEqual(len(domains), 1)
+ self.assertEqual(domains[0].mail_host, 'example.com')
+ # There should be exactly one mailing list defined.
+ mlists = list(getUtility(IListManager).mailing_lists)
+ self.assertEqual(len(mlists), 1)
+ # Get the mailing list object and check its acceptable aliases.
+ # This tests that foreign keys continue to work.
+ aliases_set = IAcceptableAliasSet(mlists)
+ self.assertEqual(set(aliases_set.aliases),
+ set(['foo@example.com', 'bar@example.com']))
diff --git a/src/mailman/interfaces/archiver.py b/src/mailman/interfaces/archiver.py
index f3edc7719..1f7e57ef0 100644
--- a/src/mailman/interfaces/archiver.py
+++ b/src/mailman/interfaces/archiver.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'ArchivePolicy',
'ClobberDate',
'IArchiver',
]
@@ -31,6 +32,13 @@ from zope.interface import Interface, Attribute
+class ArchivePolicy(Enum):
+ never = 0
+ private = 1
+ public = 2
+
+
+
class ClobberDate(Enum):
never = 1
maybe = 2
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index bced070d3..485fa92bc 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -250,6 +250,13 @@ class IMailingList(Interface):
# Delivery.
+ archive_policy = Attribute(
+ """The policy for archiving messages to this mailing list.
+
+ The value is an `ArchivePolicy` enum. Use this to archive the mailing
+ list publicly, privately, or not at all.
+ """)
+
last_post_at = Attribute(
"""The date and time a message was last posted to the mailing list.""")
@@ -511,6 +518,9 @@ class IMailingList(Interface):
without any other checks.
""")
+ newsgroup_moderation = Attribute(
+ """The moderation policy for the linked newsgroup, if there is one.""")
+
# Bounces.
forward_unrecognized_bounces_to = Attribute(
diff --git a/src/mailman/interfaces/nntp.py b/src/mailman/interfaces/nntp.py
index d5d08d3f0..61063236c 100644
--- a/src/mailman/interfaces/nntp.py
+++ b/src/mailman/interfaces/nntp.py
@@ -17,7 +17,7 @@
__metaclass__ = type
__all__ = [
- 'NewsModeration',
+ 'NewsgroupModeration',
]
@@ -25,7 +25,7 @@ from flufl.enum import Enum
-class NewsModeration(Enum):
+class NewsgroupModeration(Enum):
# The newsgroup is not moderated.
none = 0
# The newsgroup is moderated, but allows for an open posting policy.
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index bff4fbf88..294daa566 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -40,6 +40,7 @@ from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.action import Action, FilterAction
from mailman.interfaces.address import IAddress
+from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.digests import DigestFrequency
@@ -51,7 +52,7 @@ from mailman.interfaces.mailinglist import (
from mailman.interfaces.member import (
AlreadySubscribedError, MemberRole, MissingPreferredAddressError)
from mailman.interfaces.mime import FilterType
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.user import IUser
from mailman.model import roster
from mailman.model.digests import OneLastDigest
@@ -104,9 +105,7 @@ class MailingList(Model):
admin_immed_notify = Bool()
admin_notify_mchanges = Bool()
administrivia = Bool()
- archive = Bool() # XXX
- archive_private = Bool() # XXX
- archive_volume_frequency = Int() # XXX
+ archive_policy = Enum(ArchivePolicy)
# Automatic responses.
autoresponse_grace_period = TimeDelta()
autorespond_owner = Enum(ResponseAction)
@@ -163,9 +162,8 @@ class MailingList(Model):
mime_is_default_digest = Bool()
moderator_password = RawStr()
new_member_options = Int()
- news_moderation = Enum(NewsModeration)
- news_prefix_subject_too = Bool()
- nntp_host = Unicode()
+ newsgroup_moderation = Enum(NewsgroupModeration)
+ nntp_prefix_subject_too = Bool()
nondigestable = Bool()
nonmember_rejection_notice = Unicode()
obscure_addresses = Bool()
diff --git a/src/mailman/rules/docs/news-moderation.rst b/src/mailman/rules/docs/news-moderation.rst
index c695740fa..651c21bb4 100644
--- a/src/mailman/rules/docs/news-moderation.rst
+++ b/src/mailman/rules/docs/news-moderation.rst
@@ -16,8 +16,8 @@ directly to the mailing list.
Set the list configuration variable to enable newsgroup moderation.
- >>> from mailman.interfaces.nntp import NewsModeration
- >>> mlist.news_moderation = NewsModeration.moderated
+ >>> from mailman.interfaces.nntp import NewsgroupModeration
+ >>> mlist.newsgroup_moderation = NewsgroupModeration.moderated
And now all messages will match the rule.
@@ -32,6 +32,6 @@ And now all messages will match the rule.
When moderation is turned off, the rule does not match.
- >>> mlist.news_moderation = NewsModeration.none
+ >>> mlist.news_moderation = NewsgroupModeration.none
>>> rule.check(mlist, msg, {})
False
diff --git a/src/mailman/rules/news_moderation.py b/src/mailman/rules/news_moderation.py
index 1e820a61f..bd6eb6166 100644
--- a/src/mailman/rules/news_moderation.py
+++ b/src/mailman/rules/news_moderation.py
@@ -28,7 +28,7 @@ __all__ = [
from zope.interface import implementer
from mailman.core.i18n import _
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.rules import IRule
@@ -46,4 +46,4 @@ class ModeratedNewsgroup:
def check(self, mlist, msg, msgdata):
"""See `IRule`."""
- return mlist.news_moderation == NewsModeration.moderated
+ return mlist.news_moderation == NewsgroupModeration.moderated
diff --git a/src/mailman/runners/nntp.py b/src/mailman/runners/nntp.py
index 8339c735e..4b6cd414f 100644
--- a/src/mailman/runners/nntp.py
+++ b/src/mailman/runners/nntp.py
@@ -35,7 +35,7 @@ from cStringIO import StringIO
from mailman.config import config
from mailman.core.runner import Runner
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
COMMA = ','
COMMASPACE = ', '
@@ -106,8 +106,8 @@ def prepare_message(mlist, msg, msgdata):
# software to accept the posting, and not forward it on to the n.g.'s
# moderation address. The posting would not have gotten here if it hadn't
# already been approved. 1 == open list, mod n.g., 2 == moderated
- if mlist.news_moderation in (NewsModeration.open_moderated,
- NewsModeration.moderated):
+ if mlist.newsgroup_moderation in (NewsgroupModeration.open_moderated,
+ NewsgroupModeration.moderated):
del msg['approved']
msg['Approved'] = mlist.posting_address
# Should we restore the original, non-prefixed subject for gatewayed
@@ -116,9 +116,7 @@ def prepare_message(mlist, msg, msgdata):
# came from mailing list user.
stripped_subject = msgdata.get('stripped_subject',
msgdata.get('original_subject'))
- # XXX 2012-03-31 BAW: rename news_prefix_subject_too to nntp_. This
- # requires a schema change.
- if not mlist.news_prefix_subject_too and stripped_subject is not None:
+ if not mlist.nntp_prefix_subject_too and stripped_subject is not None:
del msg['subject']
msg['subject'] = stripped_subject
# Add the appropriate Newsgroups header. Multiple Newsgroups headers are
diff --git a/src/mailman/runners/tests/test_nntp.py b/src/mailman/runners/tests/test_nntp.py
index 426e829d8..477bccfa3 100644
--- a/src/mailman/runners/tests/test_nntp.py
+++ b/src/mailman/runners/tests/test_nntp.py
@@ -33,7 +33,7 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.config import config
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.runners import nntp
from mailman.testing.helpers import (
LogFileMark,
@@ -67,7 +67,7 @@ Testing
# Approved header, which NNTP software uses to forward to the
# newsgroup. The message would not have gotten to the mailing list if
# it wasn't already approved.
- self._mlist.news_moderation = NewsModeration.moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.moderated
nntp.prepare_message(self._mlist, self._msg, {})
self.assertEqual(self._msg['approved'], 'test@example.com')
@@ -76,14 +76,14 @@ Testing
# message will get an Approved header, which NNTP software uses to
# forward to the newsgroup. The message would not have gotten to the
# mailing list if it wasn't already approved.
- self._mlist.news_moderation = NewsModeration.open_moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.open_moderated
nntp.prepare_message(self._mlist, self._msg, {})
self.assertEqual(self._msg['approved'], 'test@example.com')
def test_moderation_removes_previous_approved_header(self):
# Any existing Approved header is removed from moderated messages.
self._msg['Approved'] = 'a bogus approval'
- self._mlist.news_moderation = NewsModeration.moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.moderated
nntp.prepare_message(self._mlist, self._msg, {})
headers = self._msg.get_all('approved')
self.assertEqual(len(headers), 1)
@@ -92,7 +92,7 @@ Testing
def test_open_moderation_removes_previous_approved_header(self):
# Any existing Approved header is removed from moderated messages.
self._msg['Approved'] = 'a bogus approval'
- self._mlist.news_moderation = NewsModeration.open_moderated
+ self._mlist.newsgroup_moderation = NewsgroupModeration.open_moderated
nntp.prepare_message(self._mlist, self._msg, {})
headers = self._msg.get_all('approved')
self.assertEqual(len(headers), 1)
@@ -102,7 +102,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = False
+ self._mlist.nntp_prefix_subject_too = False
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(stripped_subject='Your test')
@@ -115,7 +115,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = False
+ self._mlist.nntp_prefix_subject_too = False
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(original_subject='Your test')
@@ -128,7 +128,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = True
+ self._mlist.nntp_prefix_subject_too = True
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(stripped_subject='Your test')
@@ -141,7 +141,7 @@ Testing
# The cook-headers handler adds the original and/or stripped (of the
# prefix) subject to the metadata. Assume that handler's been run;
# check the Subject header.
- self._mlist.news_prefix_subject_too = True
+ self._mlist.nntp_prefix_subject_too = True
del self._msg['subject']
self._msg['subject'] = 'Re: Your test'
msgdata = dict(original_subject='Your test')
diff --git a/src/mailman/styles/default.py b/src/mailman/styles/default.py
index b5304528c..6108831b7 100644
--- a/src/mailman/styles/default.py
+++ b/src/mailman/styles/default.py
@@ -36,7 +36,7 @@ from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.styles import IStyle
@@ -174,10 +174,10 @@ from: .*@uplinkpro.com
mlist.linked_newsgroup = ''
mlist.gateway_to_news = False
mlist.gateway_to_mail = False
- mlist.news_prefix_subject_too = True
+ mlist.nntp_prefix_subject_too = True
# In patch #401270, this was called newsgroup_is_moderated, but the
# semantics weren't quite the same.
- mlist.news_moderation = NewsModeration.none
+ mlist.newsgroup_moderation = NewsgroupModeration.none
# Topics
#
# `topics' is a list of 4-tuples of the following form:
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 62e3b9d2a..4091c1cd6 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -25,6 +25,7 @@ __all__ = [
'TestableMaster',
'body_line_iterator',
'call_api',
+ 'chdir',
'configuration',
'digest_mbox',
'event_subscribers',
@@ -35,6 +36,7 @@ __all__ = [
'reset_the_world',
'specialized_message_from_string',
'subscribe',
+ 'temporary_db',
'wait_for_webservice',
]
@@ -394,6 +396,34 @@ class configuration:
+@contextmanager
+def temporary_db(db):
+ real_db = config.db
+ config.db = db
+ try:
+ yield
+ finally:
+ config.db = real_db
+
+
+
+class chdir:
+ """A context manager for temporary directory changing."""
+ def __init__(self, directory):
+ self._curdir = None
+ self._directory = directory
+
+ def __enter__(self):
+ self._curdir = os.getcwd()
+ os.chdir(self._directory)
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ os.chdir(self._curdir)
+ # Don't suppress exceptions.
+ return False
+
+
+
def subscribe(mlist, first_name, role=MemberRole.member):
"""Helper for subscribing a sample person to a mailing list."""
user_manager = getUtility(IUserManager)
diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py
index a2c1ab592..329e0176a 100644
--- a/src/mailman/tests/test_documentation.py
+++ b/src/mailman/tests/test_documentation.py
@@ -38,7 +38,8 @@ import mailman
from mailman.app.lifecycle import create_list
from mailman.config import config
-from mailman.testing.helpers import call_api, specialized_message_from_string
+from mailman.testing.helpers import (
+ call_api, chdir, specialized_message_from_string)
from mailman.testing.layers import SMTPLayer
@@ -46,23 +47,6 @@ DOT = '.'
-class chdir:
- """A context manager for temporary directory changing."""
- def __init__(self, directory):
- self._curdir = None
- self._directory = directory
-
- def __enter__(self):
- self._curdir = os.getcwd()
- os.chdir(self._directory)
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- os.chdir(self._curdir)
- # Don't suppress exceptions.
- return False
-
-
-
def stop():
"""Call into pdb.set_trace()"""
# Do the import here so that you get the wacky special hacked pdb instead
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index f77d86e9a..7c562e331 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -31,7 +31,7 @@ import datetime
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
-from mailman.interfaces.nntp import NewsModeration
+from mailman.interfaces.nntp import NewsgroupModeration
@@ -47,7 +47,7 @@ TYPES = dict(
bounce_info_stale_after=seconds_to_delta,
bounce_you_are_disabled_warnings_interval=seconds_to_delta,
digest_volume_frequency=DigestFrequency,
- news_moderation=NewsModeration,
+ newsgroup_moderation=NewsgroupModeration,
personalize=Personalization,
reply_goes_to_list=ReplyToMunging,
)