Barry Warsaw pushed to branch master at mailman / Mailman
Commits: e0dcdefc by Aurélien Bompard at 2015-12-16T11:04:25Z Use the relationship cascade feature to delete key-values - - - - - 6e99812b by Aurélien Bompard at 2015-12-16T11:04:25Z Use the relationship instead of an explicit query - - - - - ed9efb35 by Aurélien Bompard at 2015-12-16T11:04:25Z Add a find method for pending requests - - - - - 30611aa0 by Aurélien Bompard at 2015-12-16T11:04:25Z Make sure that Pendables always have a type - - - - - 08e22a84 by Aurélien Bompard at 2015-12-16T11:04:25Z Improve the unit test for the find method - - - - - 85fcd446 by Aurélien Bompard at 2015-12-16T11:04:25Z Add indexes on the Pendable fields that can be queried upon. - Pendable.token: queried in the add() and confirm() methods - Pendable.expiration_date: queried on evict() - PendableKeyValue.key and PendableKeyValue.value: queried in the find() method - - - - - eedb883c by Aurélien Bompard at 2015-12-16T11:04:25Z Update the REST query to use the find() method - - - - - 44b06852 by Aurélien Bompard at 2015-12-16T11:04:25Z Handle data migration - - - - - 16ef7286 by Aurélien Bompard at 2015-12-16T11:04:25Z Add a commit to avoid a lock in PostgreSQL - - - - - 80b9617e by Barry Warsaw at 2015-12-16T11:06:53Z Resolve conflicts. - - - - - 8c83cf02 by Aurélien Bompard at 2015-12-16T11:16:07Z Handle data migration - - - - - 564ee293 by Aurélien Bompard at 2015-12-16T11:16:39Z Add a commit to avoid a lock in PostgreSQL - - - - - 1f6f1428 by Aurélien Bompard at 2015-12-16T11:17:30Z Implement suggestions from the review - - - - - a0caeb78 by Aurélien Bompard at 2015-12-16T11:17:30Z Bring back the constructor for PendedKeyValue - - - - - b4144618 by Aurélien Bompard at 2015-12-16T11:17:30Z Fix a leftover type argument - - - - - ff84c2bb by Barry Warsaw at 2015-12-16T14:52:56Z A few minor cleanups. - - - - - 1ebb7654 by Barry Warsaw at 2015-12-16T15:14:32Z Optimize pendings. Closes !68 - - - - - 16 changed files: - src/mailman/app/bounces.py - src/mailman/app/docs/moderator.rst - src/mailman/app/registrar.py - src/mailman/app/subscriptions.py - src/mailman/app/tests/test_bounces.py - src/mailman/chains/hold.py - + src/mailman/database/alembic/versions/47294d3a604_pendable_indexes.py - src/mailman/database/tests/test_migrations.py - src/mailman/interfaces/pending.py - src/mailman/model/docs/pending.rst - src/mailman/model/docs/requests.rst - src/mailman/model/pending.py - src/mailman/model/requests.py - + src/mailman/model/tests/test_pending.py - src/mailman/rest/docs/sub-moderation.rst - src/mailman/rest/sub_moderation.py Changes: ===================================== src/mailman/app/bounces.py ===================================== --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -176,6 +176,7 @@ class ProbeVERP(_BaseVERPParser): @implementer(IPendable) class _ProbePendable(dict): """The pendable dictionary for probe messages.""" + PEND_TYPE = 'probe' def send_probe(member, msg): ===================================== src/mailman/app/docs/moderator.rst ===================================== --- a/src/mailman/app/docs/moderator.rst +++ b/src/mailman/app/docs/moderator.rst @@ -168,6 +168,7 @@ however the message metadata indicates that the message has been approved. _parsemsg : False approved : True moderator_approved: True + type : data version : 3 ===================================== src/mailman/app/registrar.py ===================================== --- a/src/mailman/app/registrar.py +++ b/src/mailman/app/registrar.py @@ -43,7 +43,7 @@ log = logging.getLogger('mailman.error') @implementer(IPendable) class PendableRegistration(dict): - PEND_KEY = 'registration' + PEND_TYPE = 'registration' ===================================== src/mailman/app/subscriptions.py ===================================== --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -68,7 +68,7 @@ class WhichSubscriber(Enum): @implementer(IPendable) class Pendable(dict): - pass + PEND_TYPE = 'subscription' ===================================== src/mailman/app/tests/test_bounces.py ===================================== --- a/src/mailman/app/tests/test_bounces.py +++ b/src/mailman/app/tests/test_bounces.py @@ -207,9 +207,9 @@ Message-ID: <first> # corresponds to a record in the pending database. token = send_probe(self._member, self._msg) pendable = getUtility(IPendings).confirm(token) - self.assertEqual(len(pendable.items()), 2) + self.assertEqual(len(pendable.items()), 3) self.assertEqual(set(pendable.keys()), - set(['member_id', 'message_id'])) + set(['member_id', 'message_id', 'type'])) # member_ids are pended as unicodes. self.assertEqual(uuid.UUID(hex=pendable['member_id']), self._member.member_id) ===================================== src/mailman/chains/hold.py ===================================== --- a/src/mailman/chains/hold.py +++ b/src/mailman/chains/hold.py @@ -55,7 +55,7 @@ log = logging.getLogger('mailman.vette') @implementer(IPendable) class HeldMessagePendable(dict): - PEND_KEY = 'held message' + PEND_TYPE = 'held message' @@ -149,8 +149,7 @@ class HoldChain(TerminalChainBase): request_id = hold_message(mlist, msg, msgdata, None) # Calculate a confirmation token to send to the author of the # message. - pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY, - id=request_id) + pendable = HeldMessagePendable(id=request_id) token = getUtility(IPendings).add(pendable) # Get the language to send the response in. If the sender is a # member, then send it in the member's language, otherwise send it in ===================================== src/mailman/database/alembic/versions/47294d3a604_pendable_indexes.py ===================================== --- /dev/null +++ b/src/mailman/database/alembic/versions/47294d3a604_pendable_indexes.py @@ -0,0 +1,104 @@ +"""Pendable indexes + +Add indexes on Pendable fields that can be queried upon. + + +Revision ID: 47294d3a604 +Revises: 33bc0099223 +Create Date: 2015-12-02 11:46:47.295174 + +""" + +# revision identifiers, used by Alembic. +revision = '47294d3a604' +down_revision = '3e09bb4a5dc' + +import json +import sqlalchemy as sa + +from alembic import op + + +TYPE_CLUES = { + 'member_id': 'probe', + 'token_owner': 'subscription', + '_mod_message_id': 'data', + } + +pended_table = sa.sql.table( + 'pended', + sa.sql.column('id', sa.Integer), + ) + +keyvalue_table = sa.sql.table( + 'pendedkeyvalue', + sa.sql.column('id', sa.Integer), + sa.sql.column('key', sa.Unicode), + sa.sql.column('value', sa.Unicode), + sa.sql.column('pended_id', sa.Integer), + ) + + +def upgrade(): + op.create_index( + op.f('ix_pended_expiration_date'), 'pended', ['expiration_date'], + unique=False) + op.create_index(op.f('ix_pended_token'), 'pended', ['token'], unique=False) + op.create_index( + op.f('ix_pendedkeyvalue_key'), 'pendedkeyvalue', ['key'], unique=False) + op.create_index( + op.f('ix_pendedkeyvalue_value'), 'pendedkeyvalue', ['value'], + unique=False) + # Data migration. + connection = op.get_bind() + for pended_result in connection.execute(pended_table.select()).fetchall(): + pended_id = pended_result['id'] + keyvalues = connection.execute(keyvalue_table.select().where( + keyvalue_table.c.pended_id == pended_id + )).fetchall() + kv_type = [kv for kv in keyvalues if kv['key'] == 'type'] + if kv_type: + # Convert existing type keys from JSON to plain text. + # The (pended_id, key) tuple is unique. + kv_type = kv_type[0] + try: + new_value = json.loads(kv_type['value']) + except ValueError: + # New-style entry (or already converted). + pass + else: + connection.execute(keyvalue_table.update().where( + keyvalue_table.c.id == kv_type['id'] + ).values(value=new_value)) + else: + # Detect the type and add the corresponding type key. + keys = [kv['key'] for kv in keyvalues] + for clue_key, clue_type in TYPE_CLUES.items(): + if clue_key not in keys: + continue + # We found the type, update the DB. + connection.execute(keyvalue_table.insert().values( + key='type', value=clue_type, pended_id=pended_id)) + break + + +def downgrade(): + # Data migration. + connection = op.get_bind() + # Remove the introduced type keys. + connection.execute(keyvalue_table.delete().where(sa.and_( + keyvalue_table.c.key == 'type', + keyvalue_table.c.value.in_(TYPE_CLUES.values()) + ))) + # Convert the other type keys to JSON. + keyvalues = connection.execute(keyvalue_table.select().where( + keyvalue_table.c.key == 'type')).fetchall() + for keyvalue in keyvalues: + connection.execute(keyvalue_table.update().where( + keyvalue_table.c.id == keyvalue['id'] + ).values(value=json.dumps(keyvalue['value']))) + # Remove indexes. + op.drop_index(op.f('ix_pendedkeyvalue_value'), table_name='pendedkeyvalue') + op.drop_index(op.f('ix_pendedkeyvalue_key'), table_name='pendedkeyvalue') + op.drop_index(op.f('ix_pended_token'), table_name='pended') + op.drop_index(op.f('ix_pended_expiration_date'), table_name='pended') ===================================== src/mailman/database/tests/test_migrations.py ===================================== --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -30,6 +30,7 @@ from mailman.config import config from mailman.database.alembic import alembic_cfg from mailman.database.helpers import exists_in_db from mailman.database.model import Model +from mailman.database.transaction import transaction from mailman.testing.layers import ConfigLayer @@ -100,3 +101,71 @@ class TestMigrations(unittest.TestCase): header_match_table.select()).fetchall() self.assertEqual(results, [(1, hm[0], hm[1]) for hm in test_header_matches]) + + def test_47294d3a604_pendable_keyvalues(self): + # We have 5 pended items: + # - one is a probe request + # - one is a subscription request + # - one is a moderation request + # - one is a held message + # - one is a registration request in the new format + # + # The first three used to have no 'type' key and must be properly + # typed, the held message used to have a type key, but in JSON, and + # must be converted. + pended_table = sa.sql.table( + 'pended', + sa.sql.column('id', sa.Integer), + ) + keyvalue_table = sa.sql.table( + 'pendedkeyvalue', + sa.sql.column('id', sa.Integer), + sa.sql.column('key', sa.Unicode), + sa.sql.column('value', sa.Unicode), + sa.sql.column('pended_id', sa.Integer), + ) + def get_from_db(): + results = {} + for i in range(1, 6): + query = sa.sql.select( + [keyvalue_table.c.key, keyvalue_table.c.value] + ).where( + keyvalue_table.c.pended_id == i + ) + results[i] = dict([ + (r['key'], r['value']) for r in + config.db.store.execute(query).fetchall() + ]) + return results + # Start at the previous revision + with transaction(): + alembic.command.downgrade(alembic_cfg, '33bc0099223') + for i in range(1, 6): + config.db.store.execute(pended_table.insert().values(id=i)) + config.db.store.execute(keyvalue_table.insert().values([ + {'pended_id': 1, 'key': 'member_id', 'value': 'test-value'}, + {'pended_id': 2, 'key': 'token_owner', 'value': 'test-value'}, + {'pended_id': 3, 'key': '_mod_message_id', + 'value': 'test-value'}, + {'pended_id': 4, 'key': 'type', 'value': '"held message"'}, + {'pended_id': 5, 'key': 'type', 'value': 'registration'}, + ])) + # Upgrading. + with transaction(): + alembic.command.upgrade(alembic_cfg, '47294d3a604') + results = get_from_db() + for i in range(1, 5): + self.assertIn('type', results[i]) + self.assertEqual(results[1]['type'], 'probe') + self.assertEqual(results[2]['type'], 'subscription') + self.assertEqual(results[3]['type'], 'data') + self.assertEqual(results[4]['type'], 'held message') + self.assertEqual(results[5]['type'], 'registration') + # Downgrading. + with transaction(): + alembic.command.downgrade(alembic_cfg, '33bc0099223') + results = get_from_db() + for i in range(1, 4): + self.assertNotIn('type', results[i]) + self.assertEqual(results[4]['type'], '"held message"') + self.assertEqual(results[5]['type'], '"registration"') ===================================== src/mailman/interfaces/pending.py ===================================== --- a/src/mailman/interfaces/pending.py +++ b/src/mailman/interfaces/pending.py @@ -37,6 +37,14 @@ from zope.interface import Interface, Attribute class IPendable(Interface): """A pendable object.""" + PEND_TYPE = Attribute( + """The type of this pendable. + + Subclasses must define this attribute, and it must be a unique string; + it's used to efficiently search for all pendables of the given type. + The PEND_TYPE "type" is reserved. + """) + def keys(): """The keys of the pending event data, all of which are strings.""" @@ -95,6 +103,16 @@ class IPendings(Interface): def evict(): """Remove all pended items whose lifetime has expired.""" + def find(mlist=None, pend_type=None): + """Search for the pendables matching the given criteria. + + :param mlist: The MailingList object that the pendables must be + related to. + :param pend_type: The type of the pendables that are looked for, this + corresponds to the `PEND_TYPE` attribute. + :return: An iterator over 2-tuples of the form (token, dict). + """ + def __iter__(): """An iterator over all pendables. ===================================== src/mailman/model/docs/pending.rst ===================================== --- a/src/mailman/model/docs/pending.rst +++ b/src/mailman/model/docs/pending.rst @@ -26,10 +26,9 @@ token that can be used in urls and such. >>> from mailman.interfaces.pending import IPendable >>> @implementer(IPendable) ... class SimplePendable(dict): - ... pass + ... PEND_TYPE = 'subscription' >>> subscription = SimplePendable( - ... type='subscription', ... address='aper...@example.com', ... display_name='Anne Person', ... language='en', @@ -44,8 +43,14 @@ There's exactly one entry in the pendings database now. 1 You can *confirm* the pending, which means returning the `IPendable` structure -(as a dictionary) from the database that matches the token. If the token -isn't in the database, None is returned. +(as a dictionary) from the database that matches the token. + +All `IPendable` classes have a `PEND_TYPE` attribute which must be a string. +It is used to identify and query pendables in the database, and will be +returned as the `type` key in the dictionary. Thus `type` is a reserved key +and pendables may not otherwise set it. + +If the token isn't in the database, None is returned. >>> pendable = pendingdb.confirm(b'missing') >>> print(pendable) ===================================== src/mailman/model/docs/requests.rst ===================================== --- a/src/mailman/model/docs/requests.rst +++ b/src/mailman/model/docs/requests.rst @@ -124,6 +124,7 @@ an additional key which holds the name of the request type. _request_type: held_message bar : no foo : yes + type : data Iterating over requests @@ -146,6 +147,7 @@ over by type. _request_type: held_message bar: no foo: yes + type: data Deleting requests ===================================== src/mailman/model/pending.py ===================================== --- a/src/mailman/model/pending.py +++ b/src/mailman/model/pending.py @@ -35,8 +35,8 @@ from mailman.database.transaction import dbconnection from mailman.interfaces.pending import ( IPendable, IPended, IPendedKeyValue, IPendings) from mailman.utilities.datetime import now -from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode -from sqlalchemy.orm import relationship +from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode, and_ +from sqlalchemy.orm import aliased, relationship from zope.interface import implementer from zope.interface.verify import verifyObject @@ -49,8 +49,8 @@ class PendedKeyValue(Model): __tablename__ = 'pendedkeyvalue' id = Column(Integer, primary_key=True) - key = Column(Unicode) - value = Column(Unicode) + key = Column(Unicode, index=True) + value = Column(Unicode, index=True) pended_id = Column(Integer, ForeignKey('pended.id'), index=True) def __init__(self, key, value): @@ -66,20 +66,15 @@ class Pended(Model): __tablename__ = 'pended' id = Column(Integer, primary_key=True) - token = Column(Unicode) - expiration_date = Column(DateTime) - key_values = relationship('PendedKeyValue') - - def __init__(self, token, expiration_date): - super(Pended, self).__init__() - self.token = token - self.expiration_date = expiration_date + token = Column(Unicode, index=True) + expiration_date = Column(DateTime, index=True) + key_values = relationship('PendedKeyValue', cascade="all, delete-orphan") @implementer(IPendable) class UnpendedPendable(dict): - pass + PEND_TYPE = 'unpended' @@ -114,7 +109,13 @@ class Pendings: pending = Pended( token=token, expiration_date=now() + lifetime) + pendable_type = pendable.get('type', pendable.PEND_TYPE) + pending.key_values.append( + PendedKeyValue(key='type', value=pendable_type)) for key, value in pendable.items(): + # The type has been handled above. + if key == 'type': + continue # Both keys and values must be strings. if isinstance(key, bytes): key = key.decode('utf-8') @@ -138,17 +139,18 @@ class Pendings: 'Unexpected token count: {0}'.format(pendings.count())) pending = pendings[0] pendable = UnpendedPendable() - # Find all PendedKeyValue entries that are associated with the pending - # object's ID. Watch out for type conversions. - entries = store.query(PendedKeyValue).filter( - PendedKeyValue.pended_id == pending.id) - for keyvalue in entries: - value = json.loads(keyvalue.value) + # Iterate on PendedKeyValue entries that are associated with the + # pending object's ID. Watch out for type conversions. + for keyvalue in pending.key_values: + # The `type` key is special and served. It is not JSONified. See + # the IPendable interface for details. + if keyvalue.key == 'type': + value = keyvalue.value + else: + value = json.loads(keyvalue.value) if isinstance(value, dict) and '__encoding__' in value: value = value['value'].encode(value['__encoding__']) pendable[keyvalue.key] = value - if expunge: - store.delete(keyvalue) if expunge: store.delete(pending) return pendable @@ -158,15 +160,27 @@ class Pendings: right_now = now() for pending in store.query(Pended).all(): if pending.expiration_date < right_now: - # Find all PendedKeyValue entries that are associated with the - # pending object's ID. - q = store.query(PendedKeyValue).filter( - PendedKeyValue.pended_id == pending.id) - for keyvalue in q: - store.delete(keyvalue) store.delete(pending) @dbconnection + def find(self, store, mlist=None, pend_type=None): + query = store.query(Pended) + if mlist is not None: + pkv_alias_mlist = aliased(PendedKeyValue) + query = query.join(pkv_alias_mlist).filter(and_( + pkv_alias_mlist.key == 'list_id', + pkv_alias_mlist.value == json.dumps(mlist.list_id) + )) + if pend_type is not None: + pkv_alias_type = aliased(PendedKeyValue) + query = query.join(pkv_alias_type).filter(and_( + pkv_alias_type.key == 'type', + pkv_alias_type.value == pend_type + )) + for pending in query: + yield pending.token, self.confirm(pending.token, expunge=False) + + @dbconnection def __iter__(self, store): for pending in store.query(Pended).all(): yield pending.token, self.confirm(pending.token, expunge=False) ===================================== src/mailman/model/requests.py ===================================== --- a/src/mailman/model/requests.py +++ b/src/mailman/model/requests.py @@ -41,6 +41,8 @@ from zope.interface import implementer class DataPendable(dict): """See `IPendable`.""" + PEND_TYPE = 'data' + def update(self, mapping): # Keys and values must be strings (unicodes, but bytes values are # accepted for now). Any other types for keys are a programming ===================================== src/mailman/model/tests/test_pending.py ===================================== --- /dev/null +++ b/src/mailman/model/tests/test_pending.py @@ -0,0 +1,102 @@ +# 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 pendings.""" + +__all__ = [ + 'TestPendings', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.config import config +from mailman.interfaces.pending import IPendable, IPendings +from mailman.model.pending import PendedKeyValue +from mailman.testing.layers import ConfigLayer +from zope.component import getUtility +from zope.interface import implementer + + +@implementer(IPendable) +class SimplePendable(dict): + PEND_TYPE = 'simple' + + + +class TestPendings(unittest.TestCase): + """Test pendings.""" + + layer = ConfigLayer + + def test_delete_key_values(self): + # Deleting a pending should delete its key-values. + pendingdb = getUtility(IPendings) + subscription = SimplePendable( + type='subscription', + address='aper...@example.com', + display_name='Anne Person', + language='en', + password='xyz') + token = pendingdb.add(subscription) + self.assertEqual(pendingdb.count, 1) + pendingdb.confirm(token) + self.assertEqual(pendingdb.count, 0) + self.assertEqual(config.db.store.query(PendedKeyValue).count(), 0) + + def test_find(self): + # Test getting pendables for a mailing-list. + mlist = create_list('li...@example.com') + pendingdb = getUtility(IPendings) + subscription_1 = SimplePendable( + type='subscription', + list_id='list1.example.com') + subscription_2 = SimplePendable( + type='subscription', + list_id='list2.example.com') + subscription_3 = SimplePendable( + type='hold request', + list_id='list1.example.com') + subscription_4 = SimplePendable( + type='hold request', + list_id='list2.example.com') + token_1 = pendingdb.add(subscription_1) + pendingdb.add(subscription_2) + token_3 = pendingdb.add(subscription_3) + token_4 = pendingdb.add(subscription_4) + self.assertEqual(pendingdb.count, 4) + # Find the pending subscription in list1. + pendings = list(pendingdb.find(mlist=mlist, pend_type='subscription')) + self.assertEqual(len(pendings), 1) + self.assertEqual(pendings[0][0], token_1) + self.assertEqual(pendings[0][1]['list_id'], 'list1.example.com') + # Find all pending hold requests. + pendings = list(pendingdb.find(pend_type='hold request')) + self.assertEqual(len(pendings), 2) + self.assertSetEqual( + set((p[0], p[1]['list_id']) for p in pendings), + {(token_3, 'list1.example.com'), (token_4, 'list2.example.com')} + ) + # Find all pendings for list1. + pendings = list(pendingdb.find(mlist=mlist)) + self.assertEqual(len(pendings), 2) + self.assertSetEqual( + set((p[0], p[1]['list_id'], p[1]['type']) for p in pendings), + {(token_1, 'list1.example.com', 'subscription'), + (token_3, 'list1.example.com', 'hold request')} + ) ===================================== src/mailman/rest/docs/sub-moderation.rst ===================================== --- a/src/mailman/rest/docs/sub-moderation.rst +++ b/src/mailman/rest/docs/sub-moderation.rst @@ -51,6 +51,7 @@ The subscription request can be viewed in the REST API. list_id: ant.example.com token: ... token_owner: moderator + type: subscription when: 2005-08-01T07:49:23 http_etag: "..." start: 0 @@ -71,6 +72,7 @@ You can view an individual membership change request by providing the token list_id: ant.example.com token: ... token_owner: moderator + type: subscription when: 2005-08-01T07:49:23 ===================================== src/mailman/rest/sub_moderation.py ===================================== --- a/src/mailman/rest/sub_moderation.py +++ b/src/mailman/rest/sub_moderation.py @@ -122,22 +122,9 @@ class SubscriptionRequests(_ModerationBase, CollectionMixin): self._mlist = mlist def _get_collection(self, request): - # There's currently no better way to query the pendings database for - # all the entries that are associated with subscription holds on this - # mailing list. Brute force iterating over all the pendables. - collection = [] - for token, pendable in getUtility(IPendings): - if 'token_owner' not in pendable: - # This isn't a subscription hold. - continue - list_id = pendable.get('list_id') - if list_id != self._mlist.list_id: - # Either there isn't a list_id field, in which case it can't - # be a subscription hold, or this is a hold for some other - # mailing list. - continue - collection.append(token) - return collection + pendings = getUtility(IPendings).find( + mlist=self._mlist, pend_type='subscription') + return [token for token, pendable in pendings] def on_get(self, request, response): """/lists/listname/requests""" View it on GitLab: https://gitlab.com/mailman/mailman/compare/33a64ede9aa2a6b6c5ebbbbbfbf789bc4d41e3f2...1ebb7654978939d3ae549956b7b45ef0ea0a983a
_______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org