diff options
| -rw-r--r-- | src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py | 37 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 80 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_mailinglist.py | 38 | ||||
| -rw-r--r-- | src/mailman/rest/docs/listconf.rst | 20 | ||||
| -rw-r--r-- | src/mailman/rest/header_matches.py | 43 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_header_matches.py | 4 |
6 files changed, 110 insertions, 112 deletions
diff --git a/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py b/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py index 00064bc1e..4dade211b 100644 --- a/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py +++ b/src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py @@ -1,4 +1,4 @@ -"""Add a numerical index to sort header matches. +"""Add a numerical position column to sort header matches. Revision ID: d4fbb4fd34ca Revises: bfda02ab3a9b @@ -16,25 +16,22 @@ from mailman.database.helpers import is_sqlite def upgrade(): - op.add_column( - 'headermatch', sa.Column('index', sa.Integer(), nullable=True)) - if not is_sqlite(op.get_bind()): - op.alter_column( - 'headermatch', 'mailing_list_id', - existing_type=sa.INTEGER(), nullable=False) - op.create_index( - op.f('ix_headermatch_index'), 'headermatch', ['index'], unique=False) - op.create_index( - op.f('ix_headermatch_mailing_list_id'), 'headermatch', - ['mailing_list_id'], unique=False) + with op.batch_alter_table('headermatch') as batch_op: + batch_op.add_column( + sa.Column('position', sa.Integer(), nullable=True)) + batch_op.alter_column( + 'mailing_list_id', existing_type=sa.INTEGER(), nullable=False) + batch_op.create_index( + op.f('ix_headermatch_position'), ['position'], unique=False) + batch_op.create_index( + op.f('ix_headermatch_mailing_list_id'), ['mailing_list_id'], + unique=False) def downgrade(): - op.drop_index( - op.f('ix_headermatch_mailing_list_id'), table_name='headermatch') - op.drop_index(op.f('ix_headermatch_index'), table_name='headermatch') - if not is_sqlite(op.get_bind()): - op.alter_column( - 'headermatch', 'mailing_list_id', - existing_type=sa.INTEGER(), nullable=True) - op.drop_column('headermatch', 'index') + with op.batch_alter_table('headermatch') as batch_op: + batch_op.drop_index(op.f('ix_headermatch_mailing_list_id')) + batch_op.drop_index(op.f('ix_headermatch_position')) + batch_op.alter_column( + 'mailing_list_id', existing_type=sa.INTEGER(), nullable=True) + batch_op.drop_column('position') diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 6f59a1662..601163b1b 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -192,7 +192,7 @@ class MailingList(Model): # ORM relationships header_matches = relationship( 'HeaderMatch', backref='mailing_list', cascade="all, delete-orphan", - order_by="HeaderMatch._index") + order_by="HeaderMatch._position") def __init__(self, fqdn_listname): super().__init__() @@ -640,56 +640,56 @@ class HeaderMatch(Model): Integer, ForeignKey('mailinglist.id'), index=True, nullable=False) - _index = Column('index', Integer, index=True, default=0) + _position = Column('position', Integer, index=True, default=0) header = Column(Unicode) pattern = Column(Unicode) action = Column(Enum(Action), nullable=True) def __init__(self, **kw): - if 'index' in kw: - kw['_index'] = kw['index'] - del kw['index'] + if 'position' in kw: + kw['_position'] = kw['position'] + del kw['position'] super().__init__(**kw) @hybrid_property - def index(self): + def position(self): """See `IHeaderMatch`.""" - return self._index + return self._position - @index.setter + @position.setter @dbconnection - def index(self, store, value): + def position(self, store, value): """See `IHeaderMatch`.""" if value < 0: raise ValueError('Negative indexes are not supported') - if value == self.index: + if value == self.position: return # Nothing to do existing_count = store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self.mailing_list).count() if value >= existing_count: raise ValueError( 'There are {count} header matches for this list, ' - 'the new index cannot be {count} or higher'.format( + 'the new position cannot be {count} or higher'.format( count=existing_count)) - if value < self.index: + if value < self.position: # Moving up: header matches between the new position and the # current one must be moved down the list to make room. Those after # the current position must not be changed. for header_match in store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self.mailing_list, - HeaderMatch.index >= value, - HeaderMatch.index < self.index): - header_match._index = header_match.index + 1 - elif value > self.index: + HeaderMatch.position >= value, + HeaderMatch.position < self.position): + header_match._position = header_match.position + 1 + elif value > self.position: # Moving down: header matches between the current position and the # new one must be moved up the list to make room. Those after # the new position must not be changed. for header_match in store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self.mailing_list, - HeaderMatch.index > self.index, - HeaderMatch.index <= value): - header_match._index = header_match.index - 1 - self._index = value + HeaderMatch.position > self.position, + HeaderMatch.position <= value): + header_match._position = header_match.position - 1 + self._position = value @@ -720,15 +720,15 @@ class HeaderMatchList: HeaderMatch.pattern == pattern).count() if existing > 0: raise ValueError('Pattern already exists') - last_index = store.query(HeaderMatch.index).filter( + last_position = store.query(HeaderMatch.position).filter( HeaderMatch.mailing_list == self._mailing_list - ).order_by(HeaderMatch.index.desc()).limit(1).scalar() - if last_index is None: - last_index = -1 + ).order_by(HeaderMatch.position.desc()).limit(1).scalar() + if last_position is None: + last_position = -1 header_match = HeaderMatch( mailing_list=self._mailing_list, header=header, pattern=pattern, action=action, - index=last_index + 1) + position=last_position + 1) store.add(header_match) store.expire(self._mailing_list, ['header_matches']) @@ -741,7 +741,7 @@ class HeaderMatchList: HeaderMatch.header == header.lower(), HeaderMatch.pattern == pattern, HeaderMatch.action == action).one() - header_match.index = index + header_match.position = index store.expire(self._mailing_list, ['header_matches']) @dbconnection @@ -758,7 +758,7 @@ class HeaderMatchList: raise ValueError('Pattern does not exist') else: store.delete(existing) - self._restore_index_sequence() + self._restore_position_sequence() store.expire(self._mailing_list, ['header_matches']) @dbconnection @@ -768,7 +768,7 @@ class HeaderMatchList: try: return store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list, - HeaderMatch.index == key).one() + HeaderMatch.position == key).one() except NoResultFound: raise IndexError @@ -777,12 +777,12 @@ class HeaderMatchList: try: existing = store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list, - HeaderMatch.index == key).one() + HeaderMatch.position == key).one() except NoResultFound: raise IndexError else: store.delete(existing) - self._restore_index_sequence() + self._restore_position_sequence() store.expire(self._mailing_list, ['header_matches']) @dbconnection @@ -794,19 +794,19 @@ class HeaderMatchList: def __iter__(self, store): yield from store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list - ).order_by(HeaderMatch.index) + ).order_by(HeaderMatch.position) @dbconnection - def _restore_index_sequence(self, store): - """Restore a continuous index sequence for this mailing list's header - matches. + def _restore_position_sequence(self, store): + """Restore a continuous position sequence for this mailing list's + header matches. - The header match indexes may not be continuous after deleting an item. - It won't prevent this component from working properly, but it's cleaner - to restore a continuous sequence. + The header match positions may not be continuous after deleting an + item. It won't prevent this component from working properly, but it's + cleaner to restore a continuous sequence. """ - for index, header_match in enumerate(store.query(HeaderMatch).filter( + for position, header_match in enumerate(store.query(HeaderMatch).filter( HeaderMatch.mailing_list == self._mailing_list - ).order_by(HeaderMatch.index)): - header_match._index = index + ).order_by(HeaderMatch.position)): + header_match._position = position store.expire(self._mailing_list, ['header_matches']) diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py index 61ab0901d..29b721cf4 100644 --- a/src/mailman/model/tests/test_mailinglist.py +++ b/src/mailman/model/tests/test_mailinglist.py @@ -287,17 +287,17 @@ class TestHeaderMatch(unittest.TestCase): header_matches.append('header-2', 'pattern') header_matches.append('header-3', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], [ + [(match.header, match.position) for match in header_matches], [ ('header-0', 0), ('header-1', 1), ('header-2', 2), ('header-3', 3), ]) header_match_2 = self._mlist.header_matches[2] - self.assertEqual(header_match_2.index, 2) - header_match_2.index = 1 + self.assertEqual(header_match_2.position, 2) + header_match_2.position = 1 self.assertEqual( - [(match.header, match.index) for match in header_matches], [ + [(match.header, match.position) for match in header_matches], [ ('header-0', 0), ('header-2', 1), ('header-1', 2), @@ -311,17 +311,17 @@ class TestHeaderMatch(unittest.TestCase): header_matches.append('header-2', 'pattern') header_matches.append('header-3', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], [ + [(match.header, match.position) for match in header_matches], [ ('header-0', 0), ('header-1', 1), ('header-2', 2), ('header-3', 3), ]) header_match_1 = self._mlist.header_matches[1] - self.assertEqual(header_match_1.index, 1) - header_match_1.index = 2 + self.assertEqual(header_match_1.position, 1) + header_match_1.position = 2 self.assertEqual( - [(match.header, match.index) for match in header_matches], [ + [(match.header, match.position) for match in header_matches], [ ('header-0', 0), ('header-2', 1), ('header-1', 2), @@ -334,13 +334,13 @@ class TestHeaderMatch(unittest.TestCase): header_matches.append('header-1', 'pattern') header_matches.append('header-2', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-0', 0), ('header-1', 1), ('header-2', 2)]) header_match_1 = self._mlist.header_matches[1] - self.assertEqual(header_match_1.index, 1) - header_match_1.index = 1 + self.assertEqual(header_match_1.position, 1) + header_match_1.position = 1 self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-0', 0), ('header-1', 1), ('header-2', 2)]) def test_move_negative(self): @@ -348,25 +348,25 @@ class TestHeaderMatch(unittest.TestCase): header_matches.append('header', 'pattern') header_match = self._mlist.header_matches[0] with self.assertRaises(ValueError): - header_match.index = -1 + header_match.position = -1 def test_move_invalid(self): header_matches = IHeaderMatchList(self._mlist) header_matches.append('header', 'pattern') header_match = self._mlist.header_matches[0] with self.assertRaises(ValueError): - header_match.index = 2 + header_match.position = 2 def test_insert(self): header_matches = IHeaderMatchList(self._mlist) header_matches.append('header-0', 'pattern') header_matches.append('header-1', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-0', 0), ('header-1', 1)]) header_matches.insert(1, 'header-2', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-0', 0), ('header-2', 1), ('header-1', 2)]) def test_rebuild_sequence_after_remove(self): @@ -375,15 +375,15 @@ class TestHeaderMatch(unittest.TestCase): header_matches.append('header-1', 'pattern') header_matches.append('header-2', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-0', 0), ('header-1', 1), ('header-2', 2)]) del header_matches[0] self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-1', 0), ('header-2', 1)]) header_matches.remove('header-1', 'pattern') self.assertEqual( - [(match.header, match.index) for match in header_matches], + [(match.header, match.position) for match in header_matches], [('header-2', 0)]) def test_remove_non_existent_by_index(self): diff --git a/src/mailman/rest/docs/listconf.rst b/src/mailman/rest/docs/listconf.rst index 4ad19bb13..74a58afbf 100644 --- a/src/mailman/rest/docs/listconf.rst +++ b/src/mailman/rest/docs/listconf.rst @@ -330,8 +330,8 @@ New header matches can be created by POSTing to the resource. ... '/header-matches/0') header: x-spam-flag http_etag: "..." - index: 0 pattern: ^Yes + position: 0 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/0 @@ -358,11 +358,11 @@ to a valid action. action: discard header: x-spam-status http_etag: "..." - index: 1 pattern: ^Yes + position: 1 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/1 -The resource can be changed by PATCHing it. The ``index`` key can be used to +The resource can be changed by PATCHing it. The ``position`` key can be used to change the priority of the header match in the list. If it is not supplied, the priority is not changed. @@ -379,13 +379,13 @@ priority is not changed. action: accept header: x-spam-status http_etag: "..." - index: 1 pattern: ^No + position: 1 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/1 >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com' ... '/header-matches/1', - ... dict(index=0), + ... dict(position=0), ... 'PATCH') content-length: 0 date: ... @@ -397,20 +397,20 @@ priority is not changed. action: accept header: x-spam-status http_etag: "..." - index: 0 pattern: ^No + position: 0 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/0 entry 1: header: x-spam-flag http_etag: "..." - index: 1 pattern: ^Yes + position: 1 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/1 http_etag: "..." start: 0 total_size: 2 -The PUT method can replace an entire header match. The ``index`` key is +The PUT method can replace an entire header match. The ``position`` key is optional: if it is omitted, the order will not be changed. >>> dump_json('http://localhost:9001/3.0/lists/ant.example.com' @@ -429,8 +429,8 @@ optional: if it is omitted, the order will not be changed. action: hold header: x-spam-status http_etag: "..." - index: 1 pattern: ^Yes + position: 1 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/1 A header match can be removed using the DELETE method. @@ -447,8 +447,8 @@ A header match can be removed using the DELETE method. action: accept header: x-spam-status http_etag: "..." - index: 0 pattern: ^No + position: 0 self_link: http://localhost:9001/3.0/lists/ant.example.com/header-matches/0 http_etag: "..." start: 0 diff --git a/src/mailman/rest/header_matches.py b/src/mailman/rest/header_matches.py index d93a90fd6..c5fe88184 100644 --- a/src/mailman/rest/header_matches.py +++ b/src/mailman/rest/header_matches.py @@ -42,16 +42,17 @@ class _HeaderMatchBase: self._mlist = mlist self.header_matches = IHeaderMatchList(self._mlist) - def _location(self, index): - return self.api.path_to('lists/{}/header-matches/{}'.format(self._mlist.list_id, index)) + def _location(self, position): + return self.api.path_to('lists/{}/header-matches/{}'.format( + self._mlist.list_id, position)) def _resource_as_dict(self, header_match): """See `CollectionMixin`.""" resource = dict( - index=header_match.index, + position=header_match.position, header=header_match.header, pattern=header_match.pattern, - self_link=self._location(header_match.index), + self_link=self._location(header_match.position), ) if header_match.action is not None: resource['action'] = header_match.action @@ -61,50 +62,50 @@ class _HeaderMatchBase: class HeaderMatch(_HeaderMatchBase): """A header match.""" - def __init__(self, mlist, index): + def __init__(self, mlist, position): super().__init__(mlist) - self._index = index + self._position = position def on_get(self, request, response): """Get a header match.""" try: - header_match = self.header_matches[self._index] + header_match = self.header_matches[self._position] except IndexError: - not_found(response, 'No header match at this index: {}'.format( - self._index)) + not_found(response, 'No header match at this position: {}'.format( + self._position)) else: okay(response, etag(self._resource_as_dict(header_match))) def on_delete(self, request, response): """Remove a header match.""" try: - del self.header_matches[self._index] + del self.header_matches[self._position] except IndexError: - not_found(response, 'No header match at this index: {}'.format( - self._index)) + not_found(response, 'No header match at this position: {}'.format( + self._position)) else: no_content(response) def patch_put(self, request, response, is_optional): """Update the header match.""" try: - header_match = self.header_matches[self._index] + header_match = self.header_matches[self._position] except IndexError: - not_found(response, 'No header match at this index: {}'.format( - self._index)) + not_found(response, 'No header match at this position: {}'.format( + self._position)) return kws = dict( header=GetterSetter(lowercase), pattern=GetterSetter(str), - index=GetterSetter(int), + position=GetterSetter(int), action=GetterSetter(enum_validator(Action)), ) if is_optional: # For a PATCH, all attributes are optional. kws['_optional'] = kws.keys() else: - # For a PUT, index can remain unchanged and action can be None. - kws['_optional'] = ('action', 'index') + # For a PUT, position can remain unchanged and action can be None. + kws['_optional'] = ('action', 'position') try: Validator(**kws).update(header_match, request) except ValueError as error: @@ -153,13 +154,13 @@ class HeaderMatches(_HeaderMatchBase, CollectionMixin): bad_request(response, b'This header match already exists') else: header_match = self.header_matches[-1] - created(response, self._location(header_match.index)) + created(response, self._location(header_match.position)) def on_delete(self, request, response): """Delete all header matches for this mailing list.""" self.header_matches.clear() no_content(response) - @child(r'^(?P<index>\d+)') + @child(r'^(?P<position>\d+)') def header_match(self, request, segments, **kw): - return HeaderMatch(self._mlist, int(kw['index'])) + return HeaderMatch(self._mlist, int(kw['position'])) diff --git a/src/mailman/rest/tests/test_header_matches.py b/src/mailman/rest/tests/test_header_matches.py index 4568a2ebf..f2a957d79 100644 --- a/src/mailman/rest/tests/test_header_matches.py +++ b/src/mailman/rest/tests/test_header_matches.py @@ -45,7 +45,7 @@ class TestHeaderMatches(unittest.TestCase): '/header-matches/0') self.assertEqual(cm.exception.code, 404) self.assertEqual(cm.exception.reason, - b'No header match at this index: 0') + b'No header match at this position: 0') def test_delete_missing_hm(self): with self.assertRaises(HTTPError) as cm: @@ -54,7 +54,7 @@ class TestHeaderMatches(unittest.TestCase): method='DELETE') self.assertEqual(cm.exception.code, 404) self.assertEqual(cm.exception.reason, - b'No header match at this index: 0') + b'No header match at this position: 0') def test_add_duplicate(self): header_matches = IHeaderMatchList(self._mlist) |
