diff options
| -rw-r--r-- | buildout.cfg | 2 | ||||
| -rw-r--r-- | src/mailman/interfaces/mailinglist.py | 9 | ||||
| -rw-r--r-- | src/mailman/model/docs/mailinglist.txt | 73 | ||||
| -rw-r--r-- | src/mailman/model/docs/membership.txt | 1 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 13 | ||||
| -rw-r--r-- | src/mailman/rest/adapters.py | 9 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.txt | 26 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 29 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 37 | ||||
| -rw-r--r-- | src/mailman/testing/helpers.py | 18 | ||||
| -rw-r--r-- | src/mailman/testing/layers.py | 29 |
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 |
