summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAurélien Bompard2016-02-29 11:59:29 +0100
committerBarry Warsaw2016-02-29 21:52:13 -0500
commit567aab277deaba88329aa25a0d1f2a8959522071 (patch)
tree80159f40e0fc4175da03f26f97f3f5ff84567406
parent65459448ee6da85878159e0ad1c8f556c1bd617e (diff)
downloadmailman-567aab277deaba88329aa25a0d1f2a8959522071.tar.gz
mailman-567aab277deaba88329aa25a0d1f2a8959522071.tar.zst
mailman-567aab277deaba88329aa25a0d1f2a8959522071.zip
-rw-r--r--src/mailman/database/alembic/versions/d4fbb4fd34ca_header_match_order.py37
-rw-r--r--src/mailman/model/mailinglist.py80
-rw-r--r--src/mailman/model/tests/test_mailinglist.py38
-rw-r--r--src/mailman/rest/docs/listconf.rst20
-rw-r--r--src/mailman/rest/header_matches.py43
-rw-r--r--src/mailman/rest/tests/test_header_matches.py4
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)