roster.py 9.91 KB
Newer Older
Barry Warsaw's avatar
Barry Warsaw committed
1
# Copyright (C) 2007-2015 by the Free Software Foundation, Inc.
2
#
Barry Warsaw's avatar
Barry Warsaw committed
3
# This file is part of GNU Mailman.
4
#
Barry Warsaw's avatar
Barry Warsaw committed
5 6 7 8
# 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.
9
#
Barry Warsaw's avatar
Barry Warsaw committed
10 11 12 13 14 15 16
# 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/>.
17

18 19 20 21 22 23 24
"""An implementation of an IRoster.

These are hard-coded rosters which know how to filter a set of members to find
the ones that fit a particular role.  These are used as the member, owner,
moderator, and administrator roster filters.
"""

25 26 27 28 29 30 31 32 33 34 35 36
__all__ = [
    'AdministratorRoster',
    'DigestMemberRoster',
    'MemberRoster',
    'Memberships',
    'ModeratorRoster',
    'OwnerRoster',
    'RegularMemberRoster',
    'Subscribers',
    ]


37
from mailman.database.transaction import dbconnection
38 39
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.roster import IRoster
40 41
from mailman.model.address import Address
from mailman.model.member import Member
42
from sqlalchemy import or_
43
from zope.interface import implementer
44 45


46

Barry Warsaw's avatar
Barry Warsaw committed
47
@implementer(IRoster)
48
class AbstractRoster:
49
    """An abstract IRoster class.
50

51 52 53
    This class takes the simple approach of implemented the 'users' and
    'addresses' properties in terms of the 'members' property.  This may not
    be the most efficient way, but it works.
54

55 56
    This requires that subclasses implement the 'members' property.
    """
57 58
    role = None

59 60 61
    def __init__(self, mlist):
        self._mlist = mlist

62 63
    @dbconnection
    def _query(self, store):
64
        return store.query(Member).filter(
65 66
            Member.list_id == self._mlist.list_id,
            Member.role == self.role)
67

68 69
    @property
    def members(self):
70
        """See `IRoster`."""
71
        for member in self._query():
72
            yield member
73

74 75 76 77 78
    @property
    def member_count(self):
        """See `IRoster`."""
        return self._query().count()

79 80
    @property
    def users(self):
81
        """See `IRoster`."""
82 83 84 85 86 87 88 89 90 91 92
        # Members are linked to addresses, which in turn are linked to users.
        # So while the 'members' attribute does most of the work, we have to
        # keep a set of unique users.  It's possible for the same user to be
        # subscribed to a mailing list multiple times with different
        # addresses.
        users = set(member.address.user for member in self.members)
        for user in users:
            yield user

    @property
    def addresses(self):
93
        """See `IRoster`."""
94 95 96 97 98
        # Every Member is linked to exactly one address so the 'members'
        # attribute does most of the work.
        for member in self.members:
            yield member.address

99
    @dbconnection
100 101 102 103 104 105
    def _get_all_memberships(self, store, email):
        # Avoid circular imports.
        from mailman.model.user import User
        # Here's a query that finds all members subscribed with an explicit
        # email address.
        members_a = store.query(Member).filter(
106
            Member.list_id == self._mlist.list_id,
107
            Member.role == self.role,
108
            Address.email == email,
109
            Member.address_id == Address.id)
110 111 112 113 114 115 116 117 118 119 120 121 122 123
        # Here's a query that finds all members subscribed with their
        # preferred address.
        members_u = store.query(Member).filter(
            Member.list_id == self._mlist.list_id,
            Member.role == self.role,
            Address.email==email,
            Member.user_id == User.id)
        return members_a.union(members_u).all()

    def get_member(self, email):
        """See ``IRoster``."""
        memberships = self._get_all_memberships(email)
        count = len(memberships)
        if count == 0:
124
            return None
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
        elif count == 1:
            return memberships[0]
        assert count == 2, 'Unexpected membership count: {}'.format(count)
        # This is the case where the email address is subscribed both
        # explicitly and indirectly through the preferred address.  By
        # definition, we return the explicit address membership only.
        return (memberships[0]
                if memberships[0]._address is not None
                else memberships[1])

    def get_memberships(self, email):
        """See ``IRoster``."""
        memberships = self._get_all_memberships(email)
        count = len(memberships)
        assert 0 <= count <= 2, 'Unexpected membership count: {}'.format(
            count)
        return memberships
142

143 144 145 146 147 148


class MemberRoster(AbstractRoster):
    """Return all the members of a list."""

    name = 'member'
149
    role = MemberRole.member
150 151


152 153 154 155 156 157 158 159

class NonmemberRoster(AbstractRoster):
    """Return all the nonmembers of a list."""

    name = 'nonmember'
    role = MemberRole.nonmember


160 161 162 163 164

class OwnerRoster(AbstractRoster):
    """Return all the owners of a list."""

    name = 'owner'
165
    role = MemberRole.owner
166 167 168 169 170 171 172



class ModeratorRoster(AbstractRoster):
    """Return all the owners of a list."""

    name = 'moderator'
173
    role = MemberRole.moderator
174 175 176 177 178 179 180 181



class AdministratorRoster(AbstractRoster):
    """Return all the administrators of a list."""

    name = 'administrator'

182 183
    @dbconnection
    def _query(self, store):
184
        return store.query(Member).filter(
185
            Member.list_id == self._mlist.list_id,
186
            or_(Member.role == MemberRole.owner,
Barry Warsaw's avatar
Barry Warsaw committed
187
                Member.role == MemberRole.moderator))
188

189
    @dbconnection
190
    def get_member(self, store, email):
191
        """See `IRoster`."""
192
        results = store.query(Member).filter(
Barry Warsaw's avatar
Barry Warsaw committed
193 194 195
            Member.list_id == self._mlist.list_id,
            or_(Member.role == MemberRole.moderator,
                Member.role == MemberRole.owner),
196
            Address.email == email,
Barry Warsaw's avatar
Barry Warsaw committed
197
            Member.address_id == Address.id)
198
        if results.count() == 0:
199
            return None
200
        elif results.count() == 1:
201 202
            return results[0]
        else:
203
            raise AssertionError(
Barry Warsaw's avatar
Barry Warsaw committed
204
                'Too many matching member results: {0}'.format(results))
205 206


207

208 209 210
class DeliveryMemberRoster(AbstractRoster):
    """Return all the members having a particular kind of delivery."""

211 212
    role = MemberRole.member

213 214 215 216 217 218 219 220
    @property
    def member_count(self):
        """See `IRoster`."""
        # XXX 2012-03-15 BAW: It would be nice to make this more efficient.
        # The problem is that you'd have to change the loop in _get_members()
        # checking the delivery mode to a query parameter.
        return len(tuple(self.members))

221 222
    @dbconnection
    def _get_members(self, store, *delivery_modes):
223 224 225 226 227 228 229
        """The set of members for a mailing list, filter by delivery mode.

        :param delivery_modes: The modes to filter on.
        :type delivery_modes: sequence of `DeliveryMode`.
        :return: A generator of members.
        :rtype: generator
        """
230 231 232
        results = store.query(Member).filter_by(
            list_id = self._mlist.list_id,
            role = MemberRole.member)
233 234 235 236 237 238
        for member in results:
            if member.delivery_mode in delivery_modes:
                yield member


class RegularMemberRoster(DeliveryMemberRoster):
239 240 241 242 243 244
    """Return all the regular delivery members of a list."""

    name = 'regular_members'

    @property
    def members(self):
245
        """See `IRoster`."""
246 247
        for member in self._get_members(DeliveryMode.regular):
            yield member
248 249 250



251
class DigestMemberRoster(DeliveryMemberRoster):
252 253
    """Return all the regular delivery members of a list."""

Barry Warsaw's avatar
Barry Warsaw committed
254
    name = 'digest_members'
255 256 257

    @property
    def members(self):
258
        """See `IRoster`."""
259 260 261 262
        for member in self._get_members(DeliveryMode.plaintext_digests,
                                        DeliveryMode.mime_digests,
                                        DeliveryMode.summary_digests):
            yield member
263 264 265 266 267 268 269 270



class Subscribers(AbstractRoster):
    """Return all subscribed members regardless of their role."""

    name = 'subscribers'

271 272
    @dbconnection
    def _query(self, store):
273
        return store.query(Member).filter_by(list_id = self._mlist.list_id)
274 275 276



Barry Warsaw's avatar
Barry Warsaw committed
277
@implementer(IRoster)
278 279 280 281 282 283 284 285
class Memberships:
    """A roster of a single user's memberships."""

    name = 'memberships'

    def __init__(self, user):
        self._user = user

286 287
    @dbconnection
    def _query(self, store):
288
        results = store.query(Member).filter(
289 290 291 292 293
            Member.user_id == self._user.id
            ).union(
                store.query(Member).join(Address).filter(
                    Address.user_id == self._user.id)
                )
294
        return results.distinct()
295 296 297 298 299 300 301 302 303 304

    @property
    def member_count(self):
        """See `IRoster`."""
        return self._query().count()

    @property
    def members(self):
        """See `IRoster`."""
        for member in self._query():
305 306 307 308
            yield member

    @property
    def users(self):
309
        """See `IRoster`."""
310 311 312 313
        yield self._user

    @property
    def addresses(self):
314
        """See `IRoster`."""
315 316 317
        for address in self._user.addresses:
            yield address

318
    @dbconnection
319
    def get_member(self, store, email):
320
        """See `IRoster`."""
321
        results = store.query(Member).filter(
322 323 324 325 326 327 328
            Member.address_id == Address.id,
            Address.user_id == self._user.id)
        if results.count() == 0:
            return None
        elif results.count() == 1:
            return results[0]
        else:
Barry Warsaw's avatar
Barry Warsaw committed
329 330 331
            raise AssertionError(
                'Too many matching member results: {0}'.format(
                    results.count()))
332 333 334 335 336 337 338

    @dbconnection
    def get_memberships(self, store, address):
        """See `IRoster`."""
        # 2015-04-14 BAW: See LP: #1444055 -- this currently exists just to
        # pass a test.
        raise NotImplementedError