Barry Warsaw pushed to branch master at mailman / Mailman

Commits:
a11e089c by Barry Warsaw at 2015-12-22T23:14:44Z
The ``mailman members`` command can now be used to display members based on
subscription roles.  Also, the positional "list" argument can now 
accept
list names or list-ids.

- - - - -


6 changed files:

- − port_me/list_owners.py
- src/mailman/commands/cli_members.py
- src/mailman/commands/docs/members.rst
- + src/mailman/commands/tests/test_members.py
- src/mailman/docs/NEWS.rst
- src/mailman/model/roster.py


Changes:

=====================================
port_me/list_owners.py deleted
=====================================
--- a/port_me/list_owners.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright (C) 2002-2015 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/>.
-
-import sys
-import optparse
-
-from zope.component import getUtility
-
-from mailman.MailList import MailList
-from mailman.core.i18n import _
-from mailman.initialize import initialize
-from mailman.interfaces.listmanager import IListManager
-from mailman.version import MAILMAN_VERSION
-
-
-
-def parseargs():
-    parser = optparse.OptionParser(version=MAILMAN_VERSION,
-                                   usage=_("""\
-%prog [options] [listname ...]
-
-List the owners of a mailing list, or all mailing lists if no list names are
-given."""))
-    parser.add_option('-w', '--with-listnames',
-                      default=False, action='store_true',
-                      help=_("""\
-Group the owners by list names and include the list names in the output.
-Otherwise, the owners will be sorted and uniquified based on the email
-address."""))
-    parser.add_option('-m', '--moderators',
-                      default=False, action='store_true',
-                      help=_('Include the list moderators in the output.'))
-    parser.add_option('-C', '--config',
-                      help=_('Alternative configuration file to use'))
-    opts, args = parser.parse_args()
-    return parser, opts, args
-
-
-
-def main():
-    parser, opts, args = parseargs()
-    initialize(opts.config)
-
-    list_manager = getUtility(IListManager)
-    listnames = set(args or list_manager.names)
-    bylist = {}
-
-    for listname in listnames:
-        mlist = list_manager.get(listname)
-        addrs = [addr.address for addr in mlist.owners.addresses]
-        if opts.moderators:
-            addrs.extend([addr.address for addr in mlist.moderators.addresses])
-        bylist[listname] = addrs
-
-    if opts.with_listnames:
-        for listname in listnames:
-            unique = set()
-            for addr in bylist[listname]:
-                unique.add(addr)
-            keys = list(unique)
-            keys.sort()
-            print listname
-            for k in keys:
-                print '\t', k
-    else:
-        unique = set()
-        for listname in listnames:
-            for addr in bylist[listname]:
-                unique.add(addr)
-        for k in sorted(unique):
-            print k
-
-
-
-if __name__ == '__main__':
-    main()


=====================================
src/mailman/commands/cli_members.py
=====================================
--- a/src/mailman/commands/cli_members.py
+++ b/src/mailman/commands/cli_members.py
@@ -23,8 +23,8 @@ __all__ = [
 
 
 import sys
-import codecs
 
+from contextlib import ExitStack
 from email.utils import formataddr, parseaddr
 from mailman.app.membership import add_member
 from mailman.core.i18n import _
@@ -32,7 +32,7 @@ from mailman.database.transaction import transactional
 from mailman.interfaces.command import ICLISubCommand
 from mailman.interfaces.listmanager import IListManager
 from mailman.interfaces.member import (
-    AlreadySubscribedError, DeliveryMode, DeliveryStatus)
+    AlreadySubscribedError, DeliveryMode, DeliveryStatus, MemberRole)
 from mailman.interfaces.subscriptions import RequestRecord
 from operator import attrgetter
 from zope.component import getUtility
@@ -63,6 +63,15 @@ class Members:
             help=_("""Display output to FILENAME instead of stdout.  FILENAME
             can be '-' to indicate standard output."""))
         command_parser.add_argument(
+            '-R', '--role',
+            default=None, metavar='ROLE',
+            choices=('any', 'owner', 'moderator', 'nonmember', 'member',
+                     'administrator'),
+            help=_("""Display only members with a given ROLE.  The role may be
+                   'any', 'member', 'nonmember', 'owner', 'moderator', or
+                   'administrator' (i.e. owners and moderators).  If not
+                   given, then delivery members are used. """))
+        command_parser.add_argument(
             '-r', '--regular',
             default=None, action='store_true',
             help=_('Display only regular delivery members.'))
@@ -89,20 +98,26 @@ class Members:
             was disabled for unknown (legacy) reasons."""))
         # Required positional argument.
         command_parser.add_argument(
-            'listname', metavar='LISTNAME', nargs=1,
+            'list', metavar='LIST', nargs=1,
             help=_("""\
-            The 'fully qualified list name', i.e. the posting address of the
-            mailing list.  It must be a valid email address and the domain
-            must be registered with Mailman.  List names are forced to lower
-            case."""))
+            The list to operate on.  This can be the fully qualified list
+            name', i.e. the posting address of the mailing list or the
+            List-ID."""))
+        command_parser.epilog = _(
+            """Display a mailing list's members, with filtering along various
+            criteria.""")
 
     def process(self, args):
         """See `ICLISubCommand`."""
-        assert len(args.listname) == 1, 'Missing mailing list name'
-        fqdn_listname = args.listname[0]
-        mlist = getUtility(IListManager).get(fqdn_listname)
+        assert len(args.list) == 1, 'Missing mailing list name'
+        list_spec = args.list[0]
+        list_manager = getUtility(IListManager)
+        if '@' in list_spec:
+            mlist = list_manager.get(list_spec)
+        else:
+            mlist = list_manager.get_by_list_id(list_spec)
         if mlist is None:
-            self.parser.error(_('No such list: $fqdn_listname'))
+            self.parser.error(_('No such list: $list_spec'))
         if args.input_filename is None:
             self.display_members(mlist, args)
         else:
@@ -116,10 +131,6 @@ class Members:
         :param args: The command line arguments.
         :type args: `argparse.Namespace`
         """
-        if args.output_filename == '-' or args.output_filename is None:
-            fp = sys.stdout
-        else:
-            fp = codecs.open(args.output_filename, 'w', 'utf-8')
         if args.digest == 'any':
             digest_types = [DeliveryMode.plaintext_digests,
                             DeliveryMode.mime_digests,
@@ -129,6 +140,7 @@ class Members:
         else:
             # Don't filter on digest type.
             pass
+
         if args.nomail is None:
             # Don't filter on delivery status.
             pass
@@ -146,31 +158,49 @@ class Members:
                             DeliveryStatus.by_moderator,
                             DeliveryStatus.unknown]
         else:
-            raise AssertionError('Unknown delivery status: %s' % args.nomail)
-        try:
-            addresses = list(mlist.members.addresses)
+            status = args.nomail
+            self.parser.error(_('Unknown delivery status: $status'))
+
+        if args.role is None:
+            # By default, filter on members.
+            roster = mlist.members
+        elif args.role == 'administrator':
+            roster = mlist.administrators
+        elif args.role == 'any':
+            roster = mlist.subscribers
+        else:
+            try:
+                roster = mlist.get_roster(MemberRole[args.role])
+            except KeyError:
+                role = args.role
+                self.parser.error(_('Unknown member role: $role'))
+
+        with ExitStack() as resources:
+            if args.output_filename == '-' or args.output_filename is None:
+                fp = sys.stdout
+            else:
+                fp = resources.enter_context(
+                    open(args.output_filename, 'w', encoding='utf-8'))
+            addresses = list(roster.addresses)
             if len(addresses) == 0:
-                print(mlist.fqdn_listname, 'has no members', file=fp)
+                print(_('$mlist.list_id has no members'), file=fp)
                 return
             for address in sorted(addresses, key=attrgetter('email')):
                 if args.regular:
-                    member = mlist.members.get_member(address.email)
+                    member = roster.get_member(address.email)
                     if member.delivery_mode != DeliveryMode.regular:
                         continue
                 if args.digest is not None:
-                    member = mlist.members.get_member(address.email)
+                    member = roster.get_member(address.email)
                     if member.delivery_mode not in digest_types:
                         continue
                 if args.nomail is not None:
-                    member = mlist.members.get_member(address.email)
+                    member = roster.get_member(address.email)
                     if member.delivery_status not in status_types:
                         continue
                 print(
                     formataddr((address.display_name, address.original_email)),
                     file=fp)
-        finally:
-            if fp is not sys.stdout:
-                fp.close()
 
     @transactional
     def add_members(self, mlist, args):
@@ -181,11 +211,12 @@ class Members:
         :param args: The command line arguments.
         :type args: `argparse.Namespace`
         """
-        if args.input_filename == '-':
-            fp = sys.stdin
-        else:
-            fp = codecs.open(args.input_filename, 'r', 'utf-8')
-        try:
+        with ExitStack() as resources:
+            if args.input_filename == '-':
+                fp = sys.stdin
+            else:
+                fp = resources.enter_context(
+                    open(args.input_filename, 'r', encoding='utf-8'))
             for line in fp:
                 # Ignore blank lines and lines that start with a '#'.
                 if line.startswith('#') or len(line.strip()) == 0:
@@ -200,8 +231,8 @@ class Members:
                 except AlreadySubscribedError:
                     # It's okay if the address is already subscribed, just
                     # print a warning and continue.
-                    print('Already subscribed (skipping):',
-                          email, display_name)
-        finally:
-            if fp is not sys.stdin:
-                fp.close()
+                    if not display_name:
+                        print(_('Already subscribed (skipping): $email'))
+                    else:
+                        print(_('Already subscribed (skipping): '
+                                '$display_name <$email>'))


=====================================
src/mailman/commands/docs/members.rst
=====================================
--- a/src/mailman/commands/docs/members.rst
+++ b/src/mailman/commands/docs/members.rst
@@ -6,15 +6,16 @@ The ``mailman members`` command allows a site administrator 
to display, add,
 and remove members from a mailing list.
 ::
 
-    >>> mlist1 = create_list('te...@example.com')
+    >>> ant = create_list('a...@example.com')
 
     >>> class FakeArgs:
     ...     input_filename = None
     ...     output_filename = None
-    ...     listname = []
+    ...     list = []
     ...     regular = False
     ...     digest = None
     ...     nomail = None
+    ...     role = None
     >>> args = FakeArgs()
 
     >>> from mailman.commands.cli_members import Members
@@ -27,19 +28,18 @@ Listing members
 You can list all the members of a mailing list by calling the command with no
 options.  To start with, there are no members of the mailing list.
 
-    >>> args.listname = [mlist1.fqdn_listname]
+    >>> args.list = ['ant.example.com']
     >>> command.process(args)
-    te...@example.com has no members
+    ant.example.com has no members
 
 Once the mailing list add some members, they will be displayed.
-::
 
     >>> from mailman.testing.helpers import subscribe
-    >>> subscribe(mlist1, 'Anne', email='a...@example.com')
-    <Member: Anne Person <a...@example.com> on te...@example.com
+    >>> subscribe(ant, 'Anne', email='a...@example.com')
+    <Member: Anne Person <a...@example.com> on a...@example.com
              as MemberRole.member>
-    >>> subscribe(mlist1, 'Bart', email='b...@example.com')
-    <Member: Bart Person <b...@example.com> on te...@example.com
+    >>> subscribe(ant, 'Bart', email='b...@example.com')
+    <Member: Bart Person <b...@example.com> on a...@example.com
              as MemberRole.member>
     >>> command.process(args)
     Anne Person <a...@example.com>
@@ -48,8 +48,8 @@ Once the mailing list add some members, they will be 
displayed.
 Members are displayed in alphabetical order based on their address.
 ::
 
-    >>> subscribe(mlist1, 'Anne', email='a...@aaaxample.com')
-    <Member: Anne Person <a...@aaaxample.com> on te...@example.com
+    >>> subscribe(ant, 'Anne', email='a...@aaaxample.com')
+    <Member: Anne Person <a...@aaaxample.com> on a...@example.com
              as MemberRole.member>
     >>> command.process(args)
     Anne Person <a...@aaaxample.com>
@@ -58,17 +58,15 @@ Members are displayed in alphabetical order based on their 
address.
 
 You can also output this list to a file.
 
-    >>> from tempfile import mkstemp
-    >>> fd, args.output_filename = mkstemp()
-    >>> import os
-    >>> os.close(fd)
-    >>> command.process(args)
-    >>> with open(args.output_filename) as fp:
-    ...     print(fp.read())
+    >>> from tempfile import NamedTemporaryFile
+    >>> with NamedTemporaryFile() as outfp:
+    ...     args.output_filename = outfp.name
+    ...     command.process(args)
+    ...     with open(args.output_filename) as infp:
+    ...         print(infp.read())
     Anne Person <a...@aaaxample.com>
     Anne Person <a...@example.com>
     Bart Person <b...@example.com>
-    >>> os.remove(args.output_filename)
     >>> args.output_filename = None
 
 The output file can also be standard out.
@@ -88,7 +86,7 @@ You can limit output to just the regular non-digest members...
 
     >>> from mailman.interfaces.member import DeliveryMode
     >>> args.regular = True
-    >>> member = mlist1.members.get_member('a...@example.com')
+    >>> member = ant.members.get_member('a...@example.com')
     >>> member.preferences.delivery_mode = DeliveryMode.plaintext_digests
     >>> command.process(args)
     Anne Person <a...@aaaxample.com>
@@ -97,7 +95,7 @@ You can limit output to just the regular non-digest members...
 ...or just the digest members.  Furthermore, you can either display all digest
 members...
 
-    >>> member = mlist1.members.get_member('a...@aaaxample.com')
+    >>> member = ant.members.get_member('a...@aaaxample.com')
     >>> member.preferences.delivery_mode = DeliveryMode.mime_digests
     >>> args.regular = False
     >>> args.digest = 'any'
@@ -132,16 +130,16 @@ status is enabled...
 
     >>> from mailman.interfaces.member import DeliveryStatus
 
-    >>> member = mlist1.members.get_member('a...@aaaxample.com')
+    >>> member = ant.members.get_member('a...@aaaxample.com')
     >>> member.preferences.delivery_status = DeliveryStatus.by_moderator
-    >>> member = mlist1.members.get_member('b...@example.com')
+    >>> member = ant.members.get_member('b...@example.com')
     >>> member.preferences.delivery_status = DeliveryStatus.by_user
 
-    >>> member = subscribe(mlist1, 'Cris', email='c...@example.com')
+    >>> member = subscribe(ant, 'Cris', email='c...@example.com')
     >>> member.preferences.delivery_status = DeliveryStatus.unknown
-    >>> member = subscribe(mlist1, 'Dave', email='d...@example.com')
+    >>> member = subscribe(ant, 'Dave', email='d...@example.com')
     >>> member.preferences.delivery_status = DeliveryStatus.enabled
-    >>> member = subscribe(mlist1, 'Elle', email='e...@example.com')
+    >>> member = subscribe(ant, 'Elle', email='e...@example.com')
     >>> member.preferences.delivery_status = DeliveryStatus.by_bounces
 
     >>> args.nomail = 'enabled'
@@ -195,23 +193,20 @@ need a file containing email addresses and full names 
that can be parsed by
 ``email.utils.parseaddr()``.
 ::
 
-    >>> mlist2 = create_list('te...@example.com')
-
-    >>> import os
-    >>> path = os.path.join(config.VAR_DIR, 'addresses.txt')
-    >>> with open(path, 'w') as fp:
+    >>> bee = create_list('b...@example.com')
+    >>> with NamedTemporaryFile('w', buffering=1, encoding='utf-8') as fp:
     ...     for address in ('aper...@example.com',
     ...                     'Bart Person <bper...@example.com>',
     ...                     'cper...@example.com (Cate Person)',
     ...                     ):
     ...         print(address, file=fp)
-
-    >>> args.input_filename = path
-    >>> args.listname = [mlist2.fqdn_listname]
-    >>> command.process(args)
+    ...     fp.flush()
+    ...     args.input_filename = fp.name
+    ...     args.list = ['bee.example.com']
+    ...     command.process(args)
 
     >>> from operator import attrgetter
-    >>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
+    >>> dump_list(bee.members.addresses, key=attrgetter('email'))
     aper...@example.com
     Bart Person <bper...@example.com>
     Cate Person <cper...@example.com>
@@ -227,15 +222,17 @@ taken from standard input.
     ...                 'fper...@example.com (Fred Person)',
     ...                 ):
     ...         print(address, file=fp)
+    >>> args.input_filename = '-'
     >>> filepos = fp.seek(0)
     >>> import sys
-    >>> sys.stdin = fp
-
-    >>> args.input_filename = '-'
-    >>> command.process(args)
-    >>> sys.stdin = sys.__stdin__
-
-    >>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
+    >>> try:
+    ...     stdin = sys.stdin
+    ...     sys.stdin = fp
+    ...     command.process(args)
+    ... finally:
+    ...     sys.stdin = stdin
+
+    >>> dump_list(bee.members.addresses, key=attrgetter('email'))
     aper...@example.com
     Bart Person <bper...@example.com>
     Cate Person <cper...@example.com>
@@ -246,7 +243,7 @@ taken from standard input.
 Blank lines and lines that begin with '#' are ignored.
 ::
 
-    >>> with open(path, 'w') as fp:
+    >>> with NamedTemporaryFile('w', buffering=1, encoding='utf-8') as fp:
     ...     for address in ('gper...@example.com',
     ...                     '# hper...@example.com',
     ...                     '   ',
@@ -254,10 +251,10 @@ Blank lines and lines that begin with '#' are ignored.
     ...                     'iper...@example.com',
     ...                     ):
     ...         print(address, file=fp)
+    ...     args.input_filename = fp.name
+    ...     command.process(args)
 
-    >>> args.input_filename = path
-    >>> command.process(args)
-    >>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
+    >>> dump_list(bee.members.addresses, key=attrgetter('email'))
     aper...@example.com
     Bart Person <bper...@example.com>
     Cate Person <cper...@example.com>
@@ -271,18 +268,18 @@ Addresses which are already subscribed are ignored, 
although a warning is
 printed.
 ::
 
-    >>> with open(path, 'w') as fp:
+    >>> with NamedTemporaryFile('w', buffering=1, encoding='utf-8') as fp:
     ...     for address in ('gper...@example.com',
     ...                     'aper...@example.com',
     ...                     'jper...@example.com',
     ...                     ):
     ...         print(address, file=fp)
-
-    >>> command.process(args)
+    ...     args.input_filename = fp.name
+    ...     command.process(args)
     Already subscribed (skipping): gper...@example.com
     Already subscribed (skipping): aper...@example.com
 
-    >>> dump_list(mlist2.members.addresses, key=attrgetter('email'))
+    >>> dump_list(bee.members.addresses, key=attrgetter('email'))
     aper...@example.com
     Bart Person <bper...@example.com>
     Cate Person <cper...@example.com>


=====================================
src/mailman/commands/tests/test_members.py
=====================================
--- /dev/null
+++ b/src/mailman/commands/tests/test_members.py
@@ -0,0 +1,153 @@
+# Copyright (C) 2015 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 the `mailman members` command."""
+
+__all__ = [
+    'TestCLIMembers',
+    ]
+
+
+import sys
+import unittest
+
+from functools import partial
+from io import StringIO
+from mailman.app.lifecycle import create_list
+from mailman.commands.cli_members import Members
+from mailman.interfaces.member import MemberRole
+from mailman.testing.helpers import subscribe
+from mailman.testing.layers import ConfigLayer
+from tempfile import NamedTemporaryFile
+from unittest.mock import patch
+
+
+
+class FakeArgs:
+    input_filename = None
+    output_filename = None
+    role = None
+    regular = None
+    digest = None
+    nomail = None
+    list = None
+
+
+class FakeParser:
+    def __init__(self):
+        self.message = None
+
+    def error(self, message):
+        self.message = message
+        sys.exit(1)
+
+
+
+class TestCLIMembers(unittest.TestCase):
+    layer = ConfigLayer
+
+    def setUp(self):
+        self._mlist = create_list('a...@example.com')
+        self.command = Members()
+        self.command.parser = FakeParser()
+        self.args = FakeArgs()
+
+    def test_no_such_list(self):
+        self.args.list = ['bee.example.com']
+        with self.assertRaises(SystemExit):
+            self.command.process(self.args)
+        self.assertEqual(self.command.parser.message,
+                         'No such list: bee.example.com')
+
+    def test_bad_delivery_status(self):
+        self.args.list = ['ant.example.com']
+        self.args.nomail = 'bogus'
+        with self.assertRaises(SystemExit):
+            self.command.process(self.args)
+        self.assertEqual(self.command.parser.message,
+                         'Unknown delivery status: bogus')
+
+    def test_role_administrator(self):
+        subscribe(self._mlist, 'Anne', role=MemberRole.owner)
+        subscribe(self._mlist, 'Bart', role=MemberRole.moderator)
+        subscribe(self._mlist, 'Cate', role=MemberRole.nonmember)
+        subscribe(self._mlist, 'Dave', role=MemberRole.member)
+        self.args.list = ['ant.example.com']
+        self.args.role = 'administrator'
+        with NamedTemporaryFile('w', encoding='utf-8') as outfp:
+            self.args.output_filename = outfp.name
+            self.command.process(self.args)
+            with open(outfp.name, 'r', encoding='utf-8') as infp:
+                lines = infp.readlines()
+        self.assertEqual(len(lines), 2)
+        self.assertEqual(lines[0], 'Anne Person <aper...@example.com>\n')
+        self.assertEqual(lines[1], 'Bart Person <bper...@example.com>\n')
+
+    def test_role_any(self):
+        subscribe(self._mlist, 'Anne', role=MemberRole.owner)
+        subscribe(self._mlist, 'Bart', role=MemberRole.moderator)
+        subscribe(self._mlist, 'Cate', role=MemberRole.nonmember)
+        subscribe(self._mlist, 'Dave', role=MemberRole.member)
+        self.args.list = ['ant.example.com']
+        self.args.role = 'any'
+        with NamedTemporaryFile('w', encoding='utf-8') as outfp:
+            self.args.output_filename = outfp.name
+            self.command.process(self.args)
+            with open(outfp.name, 'r', encoding='utf-8') as infp:
+                lines = infp.readlines()
+        self.assertEqual(len(lines), 4)
+        self.assertEqual(lines[0], 'Anne Person <aper...@example.com>\n')
+        self.assertEqual(lines[1], 'Bart Person <bper...@example.com>\n')
+        self.assertEqual(lines[2], 'Cate Person <cper...@example.com>\n')
+        self.assertEqual(lines[3], 'Dave Person <dper...@example.com>\n')
+
+    def test_role_moderator(self):
+        subscribe(self._mlist, 'Anne', role=MemberRole.owner)
+        subscribe(self._mlist, 'Bart', role=MemberRole.moderator)
+        subscribe(self._mlist, 'Cate', role=MemberRole.nonmember)
+        subscribe(self._mlist, 'Dave', role=MemberRole.member)
+        self.args.list = ['ant.example.com']
+        self.args.role = 'moderator'
+        with NamedTemporaryFile('w', encoding='utf-8') as outfp:
+            self.args.output_filename = outfp.name
+            self.command.process(self.args)
+            with open(outfp.name, 'r', encoding='utf-8') as infp:
+                lines = infp.readlines()
+        self.assertEqual(len(lines), 1)
+        self.assertEqual(lines[0], 'Bart Person <bper...@example.com>\n')
+
+    def test_bad_role(self):
+        self.args.list = ['ant.example.com']
+        self.args.role = 'bogus'
+        with self.assertRaises(SystemExit):
+            self.command.process(self.args)
+        self.assertEqual(self.command.parser.message,
+                         'Unknown member role: bogus')
+
+    def test_already_subscribed_with_display_name(self):
+        subscribe(self._mlist, 'Anne')
+        outfp = StringIO()
+        with NamedTemporaryFile('w', buffering=1, encoding='utf-8') as infp:
+            print('Anne Person <aper...@example.com>', file=infp)
+            self.args.list = ['ant.example.com']
+            self.args.input_filename = infp.name
+            with patch('builtins.print', partial(print, file=outfp)):
+                self.command.process(self.args)
+        self.assertEqual(
+           outfp.getvalue(),
+           'Already subscribed (skipping): Anne Person <aper...@example.com>\n'
+           )


=====================================
src/mailman/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -144,6 +144,9 @@ Other
  * The mailing list "data directory" has been renamed.  Instead of using the
    fqdn listname, the subdirectory inside ``[paths]list_data_dir`` now uses
    the List-ID.
+ * The ``mailman members`` command can now be used to display members based on
+   subscription roles.  Also, the positional "list" argument can now accept
+   list names or list-ids.
 
 
 3.0.0 -- "Show Don't Tell"


=====================================
src/mailman/model/roster.py
=====================================
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -39,7 +39,7 @@ from mailman.interfaces.member import DeliveryMode, MemberRole
 from mailman.interfaces.roster import IRoster
 from mailman.model.address import Address
 from mailman.model.member import Member
-from sqlalchemy import and_, or_
+from sqlalchemy import or_
 from zope.interface import implementer
 
 



View it on GitLab: 
https://gitlab.com/mailman/mailman/commit/a11e089cc1e0e5aff2502e584014295a414a43f9
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to