summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2010-06-18 19:23:35 -0400
committerBarry Warsaw2010-06-18 19:23:35 -0400
commitb3e874703bce97c121eb97dd9776da15a8b90070 (patch)
tree9d884eeb4c6fd4a5e7aad8e96ced642f57ecc7ac
parentf4e7637b1682f025cc6c8bfb172eda8b710e3218 (diff)
downloadmailman-b3e874703bce97c121eb97dd9776da15a8b90070.tar.gz
mailman-b3e874703bce97c121eb97dd9776da15a8b90070.tar.zst
mailman-b3e874703bce97c121eb97dd9776da15a8b90070.zip
-rw-r--r--buildout.cfg2
-rw-r--r--src/mailman/interfaces/mailinglist.py9
-rw-r--r--src/mailman/model/docs/mailinglist.txt73
-rw-r--r--src/mailman/model/docs/membership.txt1
-rw-r--r--src/mailman/model/mailinglist.py13
-rw-r--r--src/mailman/rest/adapters.py9
-rw-r--r--src/mailman/rest/docs/membership.txt26
-rw-r--r--src/mailman/rest/lists.py29
-rw-r--r--src/mailman/rest/members.py37
-rw-r--r--src/mailman/testing/helpers.py18
-rw-r--r--src/mailman/testing/layers.py29
11 files changed, 202 insertions, 44 deletions
diff --git a/buildout.cfg b/buildout.cfg
index 187a0cdbc..f1ee0d3ed 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -26,7 +26,7 @@ eggs =
defaults = '--tests-pattern ^tests --exit-with-status'.split()
# Hack in extra arguments to zope.testrunner.
initialization = from mailman.testing.layers import ConfigLayer;
- ConfigLayer.hack_options_parser()
+ ConfigLayer.enable_stderr()
[docs]
recipe = z3c.recipe.sphinxdoc
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index d5317828f..27ea7a2b8 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -218,6 +218,15 @@ class IMailingList(Interface):
role.
""")
+ def get_roster(role):
+ """Return the appropriate roster for the given role.
+
+ :param role: The requested roster's role.
+ :type role: MemberRole
+ :return: The requested roster.
+ :rtype: Roster
+ """
+
volume = Attribute(
"""A monotonically increasing integer sequentially assigned to each
new digest volume. The volume number may be bumped either
diff --git a/src/mailman/model/docs/mailinglist.txt b/src/mailman/model/docs/mailinglist.txt
new file mode 100644
index 000000000..687f7b39c
--- /dev/null
+++ b/src/mailman/model/docs/mailinglist.txt
@@ -0,0 +1,73 @@
+=============
+Mailing lists
+=============
+
+XXX 2010-06-18 BAW: This documentation needs a lot more detail.
+
+The mailing list is a core object in Mailman. It is uniquely identified in
+the system by its posting address, i.e. the email address you would send a
+message to in order to post a message to the mailing list. This must be fully
+qualified.
+
+ >>> alpha = create_list('alpha@example.com')
+ >>> print alpha.fqdn_listname
+ alpha@example.com
+
+The mailing list also has convenient attributes for accessing the list's short
+name (i.e. local part) and host name.
+
+ >>> print alpha.list_name
+ alpha
+ >>> print alpha.host_name
+ example.com
+
+
+Rosters
+=======
+
+Mailing list membership is represented by 'rosters'. Each mailing list has
+several rosters of members, representing the subscribers to the mailing list,
+the owners, the moderators, and so on. The rosters are defined by a
+membership role.
+
+ >>> from mailman.interfaces.member import MemberRole
+ >>> from mailman.testing.helpers import subscribe
+
+ >>> subscribe(alpha, 'Anne')
+ >>> subscribe(alpha, 'Bart')
+ >>> subscribe(alpha, 'Cris')
+ >>> subscribe(alpha, 'Anne', MemberRole.owner)
+ >>> subscribe(alpha, 'Dave', MemberRole.owner)
+ >>> subscribe(alpha, 'Elle', MemberRole.moderator)
+
+We can retrieve a roster directly...
+
+ >>> for member in alpha.members.members:
+ ... print member.address
+ Anne Person <aperson@example.com>
+ Bart Person <bperson@example.com>
+ Cris Person <cperson@example.com>
+
+...or programmatically.
+
+ >>> roster = alpha.get_roster(MemberRole.member)
+ >>> for member in roster.members:
+ ... print member.address
+ Anne Person <aperson@example.com>
+ Bart Person <bperson@example.com>
+ Cris Person <cperson@example.com>
+
+This includes the roster of owners...
+
+ >>> roster = alpha.get_roster(MemberRole.owner)
+ >>> for member in roster.members:
+ ... print member.address
+ Anne Person <aperson@example.com>
+ Dave Person <dperson@example.com>
+
+...and moderators.
+
+ >>> roster = alpha.get_roster(MemberRole.moderator)
+ >>> for member in roster.members:
+ ... print member.address
+ Elle Person <eperson@example.com>
diff --git a/src/mailman/model/docs/membership.txt b/src/mailman/model/docs/membership.txt
index 27d2d9552..41660f0d2 100644
--- a/src/mailman/model/docs/membership.txt
+++ b/src/mailman/model/docs/membership.txt
@@ -54,7 +54,6 @@ When we create a mailing list, it starts out with no members...
[]
-
Administrators
==============
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 8c82bedf2..65611f563 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -42,6 +42,7 @@ from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.mailinglist import (
IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization)
+from mailman.interfaces.member import MemberRole
from mailman.interfaces.mime import FilterType
from mailman.model import roster
from mailman.model.digests import OneLastDigest
@@ -420,6 +421,18 @@ class MailingList(Model):
self, mime_type, FilterType.pass_extension)
store.add(content_filter)
+ def get_roster(self, role):
+ """See `IMailingList`."""
+ if role is MemberRole.member:
+ return self.members
+ elif role is MemberRole.owner:
+ return self.owners
+ elif role is MemberRole.moderator:
+ return self.moderators
+ else:
+ raise TypeError(
+ 'Undefined MemberRole: {0}'.format(role))
+
class AcceptableAlias(Model):
diff --git a/src/mailman/rest/adapters.py b/src/mailman/rest/adapters.py
index 6acfe3866..1edac3a34 100644
--- a/src/mailman/rest/adapters.py
+++ b/src/mailman/rest/adapters.py
@@ -56,14 +56,11 @@ class SubscriptionService:
for fqdn_listname in sorted(list_manager.names):
mailing_list = list_manager.get(fqdn_listname)
members.extend(
- sorted((member for member in mailing_list.owners.members),
- key=address_of_member))
+ sorted(mailing_list.owners.members, key=address_of_member))
members.extend(
- sorted((member for member in mailing_list.moderators.members),
- key=address_of_member))
+ sorted(mailing_list.moderators.members, key=address_of_member))
members.extend(
- sorted((member for member in mailing_list.members.members),
- key=address_of_member))
+ sorted(mailing_list.members.members, key=address_of_member))
return members
def __iter__(self):
diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt
index 0dc2771ba..59af8f2f7 100644
--- a/src/mailman/rest/docs/membership.txt
+++ b/src/mailman/rest/docs/membership.txt
@@ -35,16 +35,7 @@ the REST interface.
>>> from zope.component import getUtility
>>> user_manager = getUtility(IUserManager)
- >>> def subscribe(mlist, first_name, role=MemberRole.member):
- ... address = '{0}person@example.com'.format(first_name[0].lower())
- ... full_name = '{0} Person'.format(first_name)
- ... person = user_manager.get_user(address)
- ... if person is None:
- ... person = user_manager.create_user(address, full_name)
- ... preferred_address = list(person.addresses)[0]
- ... preferred_address.subscribe(mlist, role)
- ... transaction.commit()
-
+ >>> from mailman.testing.helpers import subscribe
>>> subscribe(mlist_one, 'Bart')
>>> dump_json('http://localhost:8001/3.0/members')
entry 0:
@@ -115,6 +106,20 @@ address. Anna and Cris subscribe to this new mailing list.
start: 0
total_size: 5
+We can also get just the members of a single mailing list.
+
+ >>> dump_json(
+ ... 'http://localhost:8001/3.0/lists/alpha@example.com/roster/members')
+ entry 0:
+ http_etag: ...
+ self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/aperson@example.com
+ entry 1:
+ http_etag: ...
+ self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/cperson@example.com
+ http_etag: ...
+ start: 0
+ total_size: 2
+
Owners and moderators
=====================
@@ -316,4 +321,3 @@ Even using an address with "funny" characters Hugh can join the mailing list.
date: ...
location: http://localhost:8001/3.0/lists/alpha@example.com/member/hugh%2Fperson@example.com
...
-
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 2349ac286..5d7ed0c86 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -36,7 +36,7 @@ from mailman.interfaces.listmanager import (
from mailman.interfaces.member import MemberRole
from mailman.rest.helpers import (
CollectionMixin, Validator, etag, path_to, restish_matcher)
-from mailman.rest.members import AMember
+from mailman.rest.members import AMember, MembersOfList
@@ -59,6 +59,27 @@ def member_matcher(request, segments):
return (), dict(role=role, address=segments[1]), ()
+@restish_matcher
+def roster_matcher(request, segments):
+ """A matcher of all members URLs inside mailing lists.
+
+ e.g. /roster/members
+ /roster/owners
+ /roster/moderators
+
+ The URL roles are the plural form of the MemberRole enum, because the
+ former reads better.
+ """
+ if len(segments) != 2 or segments[0] != 'roster':
+ return None
+ role = segments[1][:-1]
+ try:
+ return (), dict(role=MemberRole[role]), ()
+ except ValueError:
+ # Not a valid role.
+ return None
+
+
class _ListBase(resource.Resource, CollectionMixin):
"""Shared base class for mailing list representations."""
@@ -93,8 +114,14 @@ class AList(_ListBase):
@resource.child(member_matcher)
def member(self, request, segments, role, address):
+ """Return a single member representation."""
return AMember(self._mlist, role, address)
+ @resource.child(roster_matcher)
+ def roster(self, request, segments, role):
+ """Return the collection of all a mailing list's members."""
+ return MembersOfList(self._mlist, role)
+
class AllLists(_ListBase):
"""The mailing lists."""
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index d9fd50e24..abd0eff67 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -23,9 +23,11 @@ __metaclass__ = type
__all__ = [
'AMember',
'AllMembers',
+ 'MembersOfList',
]
+from operator import attrgetter
from restish import http, resource
from urllib import quote
from zope.component import getUtility
@@ -63,17 +65,7 @@ class AMember(_MemberBase):
self._mlist = mailing_list
self._role = role
self._address = address
- # XXX 2010-02-24 barry There should be a more direct way to get a
- # member out of a mailing list.
- if self._role is MemberRole.member:
- roster = self._mlist.members
- elif self._role is MemberRole.owner:
- roster = self._mlist.owners
- elif self._role is MemberRole.moderator:
- roster = self._mlist.moderators
- else:
- raise AssertionError(
- 'Undefined MemberRole: {0}'.format(self._role))
+ roster = self._mlist.get_roster(role)
self._member = roster.get_member(self._address)
@resource.GET()
@@ -130,3 +122,26 @@ class AllMembers(_MemberBase):
"""/members"""
resource = self._make_collection(request)
return http.ok([], etag(resource))
+
+
+
+class MembersOfList(_MemberBase):
+ """The members of a mailing list."""
+
+ def __init__(self, mailing_list, role):
+ self._mlist = mailing_list
+ self._role = role
+
+ def _get_collection(self, request):
+ """See `CollectionMixin`."""
+ # Overrides _MemberBase._get_collection() because we only want to
+ # return the members from the requested roster.
+ roster = self._mlist.get_roster(self._role)
+ address_of_member = attrgetter('address.address')
+ return list(sorted(roster.members, key=address_of_member))
+
+ @resource.GET()
+ def container(self, request):
+ """roster/[members|owners|moderators]"""
+ resource = self._make_collection(request)
+ return http.ok([], etag(resource))
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index b9936ac71..67e2ad21a 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -27,6 +27,7 @@ __all__ = [
'get_lmtp_client',
'get_queue_messages',
'make_testable_runner',
+ 'subscribe',
'wait_for_webservice',
]
@@ -43,9 +44,12 @@ import threading
from contextlib import contextmanager
from zope import event
+from zope.component import getUtility
from mailman.bin.master import Loop as Master
from mailman.config import config
+from mailman.interfaces.member import MemberRole
+from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.mailbox import Mailbox
@@ -252,3 +256,17 @@ def event_subscribers(*subscribers):
event.subscribers = list(subscribers)
yield
event.subscribers[:] = old_subscribers
+
+
+
+def subscribe(mlist, first_name, role=MemberRole.member):
+ """Helper for subscribing a sample person to a mailing list."""
+ user_manager = getUtility(IUserManager)
+ address = '{0}person@example.com'.format(first_name[0].lower())
+ full_name = '{0} Person'.format(first_name)
+ person = user_manager.get_user(address)
+ if person is None:
+ person = user_manager.create_user(address, full_name)
+ preferred_address = list(person.addresses)[0]
+ preferred_address.subscribe(mlist, role)
+ config.db.commit()
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index e7907b172..34dc46807 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -197,22 +197,25 @@ class ConfigLayer(MockAndMonkeyLayer):
stderr = False
@classmethod
- def handle_stderr(cls, *ignore):
- cls.stderr = True
+ def enable_stderr(cls):
+ """Enable stderr logging if -e/--stderr is given.
- @classmethod
- def hack_options_parser(cls):
- """Hack our way into the zc.testing framework.
+ We used to hack our way into the zc.testing framework, but that was
+ undocumented and way too fragile. Well, this probably is too, but now
+ we just scan sys.argv for -e/--stderr and enable logging if found.
+ Then we remove the option from sys.argv. This works because this
+ method is called before zope.testrunner sees the options.
- Add our custom command line option parsing into zc.testing's. We do
- the imports here so that if zc.testing isn't invoked, this stuff never
- gets in the way. This is pretty fragile, depend on changes in the
- zc.testing package. There should be a better way!
+ As a bonus, we'll check an environment variable too.
"""
- from zope.testing.testrunner.options import parser
- parser.add_option(str('-e'), str('--stderr'),
- action='callback', callback=cls.handle_stderr,
- help=_('Propagate log errors to stderr.'))
+ if '-e' in sys.argv:
+ cls.stderr = True
+ sys.argv.remove('-e')
+ if '--stderr' in sys.argv:
+ cls.stderr = True
+ sys.argv.remove('--stderr')
+ if len(os.environ.get('MM_VERBOSE_TESTLOG', '').strip()) > 0:
+ cls.stderr = True