diff options
Diffstat (limited to 'src/mailman/database')
3 files changed, 384 insertions, 0 deletions
diff --git a/src/mailman/database/alembic/versions/7c5b39d1ecc4_workflow_steps.py b/src/mailman/database/alembic/versions/7c5b39d1ecc4_workflow_steps.py new file mode 100644 index 000000000..c2daffd63 --- /dev/null +++ b/src/mailman/database/alembic/versions/7c5b39d1ecc4_workflow_steps.py @@ -0,0 +1,90 @@ +# Copyright (C) 2015-2017 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/>. + +"""Save whole workflow stack in workflowstate. + +Revision ID: 7c5b39d1ecc4 +Revises: 4bd95c99b2e7 +Create Date: 2017-07-04 16:45:36.470746 + +""" + +import json +import sqlalchemy as sa + +from alembic import op +from mailman.database.types import SAUnicode + +# revision identifiers, used by Alembic. +revision = '7c5b39d1ecc4' +down_revision = '4bd95c99b2e7' + +old_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('step', SAUnicode), + sa.sql.column('data', SAUnicode) + ) +new_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('steps', SAUnicode), + sa.sql.column('data', SAUnicode) + ) + + +def upgrade(): + # Rename the column + with op.batch_alter_table('workflowstate') as batch_op: + batch_op.alter_column('step', new_column_name='steps', + existing_type=SAUnicode) + + connection = op.get_bind() + for token, step, data in connection.execute( + new_state_table.select()).fetchall(): + # Wrap the single step in a JSON array. + if step is not None: + new_steps = json.dumps([step]) + else: + new_steps = '[]' + connection.execute( + new_state_table.update().where( + new_state_table.c.token == token).values( + steps=new_steps) + ) + + +def downgrade(): + # Rename back the column + connection = op.get_bind() + with op.batch_alter_table('workflowstate') as batch_op: + batch_op.alter_column('steps', new_column_name='step', + existing_type=SAUnicode) + + for token, steps, data in connection.execute( + old_state_table.select()).fetchall(): + # Extract and save at least the last state. + step_stack = json.loads(steps) + if len(step_stack) == 0: + new_step = None + else: + new_step = step_stack.pop() + connection.execute( + old_state_table.update().where( + old_state_table.c.token == token).values( + step=new_step) + ) diff --git a/src/mailman/database/alembic/versions/ccb9e28c44f4_mailinglist_sub_unsub_policies.py b/src/mailman/database/alembic/versions/ccb9e28c44f4_mailinglist_sub_unsub_policies.py new file mode 100644 index 000000000..3eab48de0 --- /dev/null +++ b/src/mailman/database/alembic/versions/ccb9e28c44f4_mailinglist_sub_unsub_policies.py @@ -0,0 +1,141 @@ +# Copyright (C) 2015-2017 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/>. + +"""Change the subscription_policy and unsubscription_policy attribute type. + +Revision ID: ccb9e28c44f4 +Revises: 7c5b39d1ecc4 +Create Date: 2017-07-05 00:02:18.238155 + +Changes the [un]subscription_policy MailingList attributes to a string keyƤ +of the workflow name from an Enum. +""" + +import sqlalchemy as sa + +from alembic import op + +from mailman.database.types import SAUnicode +from mailman.interfaces.mailinglist import SubscriptionPolicy +from mailman.workflows.subscription import ( + ConfirmModerationSubscriptionPolicy, ConfirmSubscriptionPolicy, + ModerationSubscriptionPolicy, OpenSubscriptionPolicy) +from mailman.workflows.unsubscription import ( + ConfirmModerationUnsubscriptionPolicy, ConfirmUnsubscriptionPolicy, + ModerationUnsubscriptionPolicy, OpenUnsubscriptionPolicy) + +# revision identifiers, used by Alembic. +revision = 'ccb9e28c44f4' +down_revision = '7c5b39d1ecc4' + + +mlist_table = sa.sql.table( + 'mailinglist', + sa.sql.column('id', sa.Integer), + sa.sql.column('old_subscription_policy', sa.Integer), + sa.sql.column('old_unsubscription_policy', sa.Integer), + sa.sql.column('subscription_policy', SAUnicode), + sa.sql.column('unsubscription_policy', SAUnicode) + ) + +sub_map = { + SubscriptionPolicy.open.value: OpenSubscriptionPolicy.name, + SubscriptionPolicy.confirm.value: ConfirmSubscriptionPolicy.name, + SubscriptionPolicy.moderate.value: ModerationSubscriptionPolicy.name, + SubscriptionPolicy.confirm_then_moderate.value: + ConfirmModerationSubscriptionPolicy.name + } + +sub_inv_map = {v: k for k, v in sub_map.items()} + +unsub_map = { + SubscriptionPolicy.open.value: OpenUnsubscriptionPolicy.name, + SubscriptionPolicy.confirm.value: ConfirmUnsubscriptionPolicy.name, + SubscriptionPolicy.moderate.value: ModerationUnsubscriptionPolicy.name, + SubscriptionPolicy.confirm_then_moderate.value: + ConfirmModerationUnsubscriptionPolicy.name + } + +unsub_inv_map = {v: k for k, v in unsub_map.items()} + + +def upgrade(): + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.alter_column('subscription_policy', + new_column_name='old_subscription_policy', + existing_type=sa.Integer, existing_nullable=True) + batch_op.alter_column('unsubscription_policy', + new_column_name='old_unsubscription_policy', + existing_type=sa.Integer, existing_nullable=True) + + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.add_column(sa.Column('subscription_policy', SAUnicode)) + batch_op.add_column(sa.Column('unsubscription_policy', SAUnicode)) + + connection = op.get_bind() + for (table_id, old_sub_policy, old_unsub_policy, sub_policy, + unsub_policy) in connection.execute(mlist_table.select()).fetchall(): + new_sub = sub_map.get(old_sub_policy, + ConfirmSubscriptionPolicy.name) + + new_unsub = unsub_map.get(old_unsub_policy, + ConfirmUnsubscriptionPolicy.name) + + connection.execute(mlist_table.update().where( + mlist_table.c.id == table_id).values( + subscription_policy=new_sub, + unsubscription_policy=new_unsub + )) + + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.drop_column('old_subscription_policy') + batch_op.drop_column('old_unsubscription_policy') + + +def downgrade(): + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.alter_column('subscription_policy', + new_column_name='old_subscription_policy', + existing_type=SAUnicode) + batch_op.alter_column('unsubscription_policy', + new_column_name='old_unsubscription_policy', + existing_type=SAUnicode) + + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.add_column(sa.Column('subscription_policy', sa.Integer, + nullable=True)) + batch_op.add_column(sa.Column('unsubscription_policy', sa.Integer, + nullable=True)) + + connection = op.get_bind() + for (table_id, old_sub_policy, old_unsub_policy, sub_policy, + unsub_policy) in connection.execute(mlist_table.select()).fetchall(): + + new_sub = sub_inv_map.get(old_sub_policy, + SubscriptionPolicy.confirm.value) + new_unsub = unsub_inv_map.get(old_unsub_policy, + SubscriptionPolicy.confirm.value) + + connection.execute(mlist_table.update().where( + mlist_table.c.id == table_id).values( + subscription_policy=new_sub, + unsubscription_policy=new_unsub + )) + + with op.batch_alter_table('mailinglist') as batch_op: + batch_op.drop_column('old_subscription_policy') + batch_op.drop_column('old_unsubscription_policy') diff --git a/src/mailman/database/tests/test_migrations.py b/src/mailman/database/tests/test_migrations.py index ce1bc101b..104487350 100644 --- a/src/mailman/database/tests/test_migrations.py +++ b/src/mailman/database/tests/test_migrations.py @@ -18,6 +18,7 @@ """Test database schema migrations with Alembic""" import os +import json import unittest import sqlalchemy as sa import alembic.command @@ -31,10 +32,13 @@ from mailman.database.transaction import transaction from mailman.database.types import Enum, SAUnicode from mailman.interfaces.action import Action from mailman.interfaces.cache import ICacheManager +from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.member import MemberRole from mailman.interfaces.template import ITemplateManager from mailman.interfaces.usermanager import IUserManager from mailman.testing.layers import ConfigLayer +from mailman.workflows.subscription import ModerationSubscriptionPolicy +from mailman.workflows.unsubscription import OpenUnsubscriptionPolicy from warnings import catch_warnings, simplefilter from zope.component import getUtility @@ -495,3 +499,152 @@ class TestMigrations(unittest.TestCase): self.assertEqual( len(list(config.db.store.execute(mlist_table.select()))), 0) + + def test_7c5b39d1ecc4_workflow_steps_upgrade(self): + old_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('step', SAUnicode), + sa.sql.column('data', SAUnicode) + ) + new_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('steps', SAUnicode), + sa.sql.column('data', SAUnicode) + ) + with transaction(): + # Start at the previous revision. + alembic.command.downgrade(alembic_cfg, '4bd95c99b2e7') + config.db.store.execute(old_state_table.insert().values([ + dict(token='12345', + step='some_step', + data='whatever data'), + dict(token='6789', + step=None, + data='other data') + ])) + + # Now upgrade. + alembic.command.upgrade(alembic_cfg, '7c5b39d1ecc4') + + token, steps, data = config.db.store.execute( + new_state_table.select().where( + new_state_table.c.token == '12345' + )).fetchone() + self.assertEqual(token, '12345') + self.assertEqual(steps, json.dumps(['some_step'])) + self.assertEqual(data, 'whatever data') + + token, steps, data = config.db.store.execute( + new_state_table.select().where( + new_state_table.c.token == '6789' + )).fetchone() + self.assertEqual(token, '6789') + self.assertEqual(steps, json.dumps([])) + self.assertEqual(data, 'other data') + + def test_7c5b39d1ecc4_workflow_steps_downgrade(self): + old_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('step', SAUnicode), + sa.sql.column('data', SAUnicode) + ) + new_state_table = sa.sql.table( + 'workflowstate', + sa.sql.column('token', SAUnicode), + sa.sql.column('steps', SAUnicode), + sa.sql.column('data', SAUnicode) + ) + with transaction(): + # Start at the revision. + alembic.command.downgrade(alembic_cfg, '7c5b39d1ecc4') + config.db.store.execute(new_state_table.insert().values([ + dict(token='12345', + steps=json.dumps(['next_step', 'some_step']), + data='whatever data'), + dict(token='6789', + steps=json.dumps(['only_step']), + data='other data'), + dict(token='abcde', + steps=json.dumps([]), + data='another data') + ])) + # Now downgrade. + alembic.command.downgrade(alembic_cfg, '4bd95c99b2e7') + + token, step, data = config.db.store.execute( + old_state_table.select().where( + old_state_table.c.token == '12345' + )).fetchone() + self.assertEqual(token, '12345') + self.assertEqual(step, 'some_step') + self.assertEqual(data, 'whatever data') + + token, step, data = config.db.store.execute( + old_state_table.select().where( + old_state_table.c.token == '6789' + )).fetchone() + self.assertEqual(token, '6789') + self.assertEqual(step, 'only_step') + self.assertEqual(data, 'other data') + + token, step, data = config.db.store.execute( + old_state_table.select().where( + old_state_table.c.token == 'abcde' + )).fetchone() + self.assertEqual(token, 'abcde') + self.assertEqual(step, None) + self.assertEqual(data, 'another data') + + def test_ccb9e28c44f4_mailinglist_sub_unsub_policies_downgrade(self): + # Downgrade to the tested revision. + alembic.command.downgrade(alembic_cfg, 'ccb9e28c44f4') + # Create our example list. + with transaction(): + mlist = create_list('test@example.com') + mlist.subscription_policy = ModerationSubscriptionPolicy + mlist.unsubscription_policy = OpenUnsubscriptionPolicy + # Downgrade, should keep the policies, since they have a value in the + # SubscriptionPolicy enum. + alembic.command.downgrade(alembic_cfg, '7c5b39d1ecc4') + old_mlist_table = sa.sql.table( + 'mailinglist', + sa.sql.column('id', sa.Integer), + sa.sql.column('subscription_policy', sa.Integer), + sa.sql.column('unsubscription_policy', sa.Integer) + ) + table_id, sub_policy, unsub_policy = config.db.store.execute( + old_mlist_table.select()).fetchone() + self.assertEqual(sub_policy, SubscriptionPolicy.moderate.value) + self.assertEqual(unsub_policy, SubscriptionPolicy.open.value) + + def test_ccb9e28c44f4_mailinglist_sub_unsub_policies_upgrade(self): + old_mlist_table = sa.sql.table( + 'mailinglist', + sa.sql.column('id', sa.Integer), + sa.sql.column('subscription_policy', sa.Integer), + sa.sql.column('unsubscription_policy', sa.Integer) + ) + with transaction(): + # Downgrade to a revision below the one tested. + alembic.command.downgrade(alembic_cfg, '7c5b39d1ecc4') + + config.db.store.execute( + old_mlist_table.insert().values( + subscription_policy=SubscriptionPolicy.moderate.value, + unsubscription_policy=SubscriptionPolicy.open.value) + ) + # Upgrade and test that the new names and types are there. + alembic.command.upgrade(alembic_cfg, 'ccb9e28c44f4') + new_mlist_table = sa.sql.table( + 'mailinglist', + sa.sql.column('id', sa.Integer), + sa.sql.column('subscription_policy', SAUnicode), + sa.sql.column('unsubscription_policy', SAUnicode) + ) + table_id, sub_policy, unsub_policy = config.db.store.execute( + new_mlist_table.select()).fetchone() + self.assertEqual(sub_policy, ModerationSubscriptionPolicy.name) + self.assertEqual(unsub_policy, OpenUnsubscriptionPolicy.name) |
