From 1eebfe7d22271ba2db0ede935a5294df82abfe00 Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Fri, 24 Jun 2016 13:46:24 -0400 Subject: [PATCH] Factor tip migration out into a mixin This works around a bug in Sphinx where the print function under Python 2.7 prevents class attributes from showing up in documentation. I want to be able to document some attributes on the Team class. --- gratipay/models/team/__init__.py | 94 +---------------- gratipay/models/team/mixins/__init__.py | 3 + gratipay/models/team/mixins/tip_migration.py | 101 +++++++++++++++++++ tests/py/test_tip_migration.py | 2 +- 4 files changed, 107 insertions(+), 93 deletions(-) create mode 100644 gratipay/models/team/mixins/__init__.py create mode 100644 gratipay/models/team/mixins/tip_migration.py diff --git a/gratipay/models/team/__init__.py b/gratipay/models/team/__init__.py index 22cb3a93c0..3805701a4f 100644 --- a/gratipay/models/team/__init__.py +++ b/gratipay/models/team/__init__.py @@ -12,6 +12,7 @@ from postgres.orm import Model from gratipay.billing.exchanges import MINIMUM_CHARGE +from gratipay.models.team.mixins import TipMigration # Should have at least one letter. TEAM_NAME_PATTERN = re.compile(r'^(?=.*[A-Za-z])([A-Za-z0-9.,-_ ]+)$') @@ -32,7 +33,7 @@ def slugize(name): return slug -class Team(Model): +class Team(Model, TipMigration): """Represent a Gratipay team. """ @@ -303,50 +304,6 @@ def to_dict(self): 'todo_url': self.todo_url } - def migrate_tips(self): - """Migrate the Team owner's Gratipay 1.0 tips into 2.0 payment instructions to the Team. - - :return: ``None`` - :raises: :py:exc:`~gratipay.models.team.AlreadyMigrated` if payment - instructions already exist for this Team - - This method gets called under :py:func:`migrate_all_tips` during payday. - - """ - payment_instructions = self.db.all(""" - SELECT pi.* - FROM payment_instructions pi - JOIN teams t ON t.id = pi.team_id - WHERE t.owner = %s - AND pi.ctime < t.ctime - """, (self.owner, )) - - # Make sure the migration hasn't been done already - if payment_instructions: - raise AlreadyMigrated - - return self.db.one(""" - WITH rows AS ( - - INSERT INTO payment_instructions - (ctime, mtime, participant_id, team_id, amount, is_funded) - SELECT ct.ctime - , ct.mtime - , (SELECT id FROM participants WHERE username=ct.tipper) - , %(team_id)s - , ct.amount - , ct.is_funded - FROM current_tips ct - JOIN participants p ON p.username = tipper - WHERE ct.tippee=%(owner)s - AND p.claimed_time IS NOT NULL - AND p.is_suspicious IS NOT TRUE - AND p.is_closed IS NOT TRUE - RETURNING 1 - - ) SELECT count(*) FROM rows; - """, {'team_id': self.id, 'owner': self.owner}) - # Images # ====== @@ -389,50 +346,3 @@ def load_image(self, size): with self.db.get_connection() as c: image = c.lobject(oid, mode='rb').read() return image - - -def migrate_all_tips(db, print=print): - """Migrate tips for all teams. - - :param GratipayDB db: a database object - :param func print: a function that takes lines of log output - :returns: ``None`` - - This function loads :py:class:`~gratipay.models.team.Team` objects for all - Teams where the owner had tips under Gratipay 1.0 but those tips have not - yet been migrated into payment instructions under Gratipay 2.0. It then - migrates the tips using :py:meth:`~gratipay.models.team.Team.migrate_tips`. - - This function is wrapped in a script, ``bin/migrate-tips.py``, which is - `used during payday`_. - - .. _used during payday: http://inside.gratipay.com/howto/run-payday - - """ - teams = db.all(""" - SELECT distinct ON (t.id) t.*::teams - FROM teams t - JOIN tips ON t.owner = tips.tippee -- Only fetch teams whose owners had tips under Gratipay 1.0 - WHERE t.is_approved IS TRUE -- Only fetch approved teams - AND NOT EXISTS ( -- Make sure tips haven't been migrated for any teams with same owner - SELECT 1 - FROM payment_instructions pi - JOIN teams t2 ON t2.id = pi.team_id - WHERE t2.owner = t.owner - AND pi.ctime < t2.ctime - ) - """) - - for team in teams: - try: - ntips = team.migrate_tips() - print("Migrated {} tip(s) for '{}'".format(ntips, team.slug)) - except AlreadyMigrated: - print("'%s' already migrated." % team.slug) - - print("Done.") - - -class AlreadyMigrated(Exception): - """Raised by :py:meth:`~gratipay.models.team.migrate_tips`. - """ diff --git a/gratipay/models/team/mixins/__init__.py b/gratipay/models/team/mixins/__init__.py new file mode 100644 index 0000000000..5be5769fb5 --- /dev/null +++ b/gratipay/models/team/mixins/__init__.py @@ -0,0 +1,3 @@ +from .tip_migration import TipMigrationMixin as TipMigration + +__all__ = ['TipMigration'] diff --git a/gratipay/models/team/mixins/tip_migration.py b/gratipay/models/team/mixins/tip_migration.py new file mode 100644 index 0000000000..bef47a3c0e --- /dev/null +++ b/gratipay/models/team/mixins/tip_migration.py @@ -0,0 +1,101 @@ +"""Participants who received tips directly under Gittipay 1.0 will have their +tips migrated if and when they become the owner of a new Gratipay 2.0 team. +""" + +from __future__ import absolute_import, division, print_function, unicode_literals + + +class TipMigrationMixin(object): + """This mixin provides tip migration for teams. + """ + + def migrate_tips(self): + """Migrate the Team owner's Gratipay 1.0 tips into 2.0 payment instructions to the Team. + + :return: ``None`` + :raises: :py:exc:`~gratipay.models.team.AlreadyMigrated` if payment + instructions already exist for this Team + + This method gets called under :py:func:`migrate_all_tips` during payday. + + """ + payment_instructions = self.db.all(""" + SELECT pi.* + FROM payment_instructions pi + JOIN teams t ON t.id = pi.team_id + WHERE t.owner = %s + AND pi.ctime < t.ctime + """, (self.owner, )) + + # Make sure the migration hasn't been done already + if payment_instructions: + raise AlreadyMigrated + + return self.db.one(""" + WITH rows AS ( + + INSERT INTO payment_instructions + (ctime, mtime, participant_id, team_id, amount, is_funded) + SELECT ct.ctime + , ct.mtime + , (SELECT id FROM participants WHERE username=ct.tipper) + , %(team_id)s + , ct.amount + , ct.is_funded + FROM current_tips ct + JOIN participants p ON p.username = tipper + WHERE ct.tippee=%(owner)s + AND p.claimed_time IS NOT NULL + AND p.is_suspicious IS NOT TRUE + AND p.is_closed IS NOT TRUE + RETURNING 1 + + ) SELECT count(*) FROM rows; + """, {'team_id': self.id, 'owner': self.owner}) + + +def migrate_all_tips(db, print=print): + """Migrate tips for all teams. + + :param GratipayDB db: a database object + :param func print: a function that takes lines of log output + :returns: ``None`` + + This function loads :py:class:`~gratipay.models.team.Team` objects for all + Teams where the owner had tips under Gratipay 1.0 but those tips have not + yet been migrated into payment instructions under Gratipay 2.0. It then + migrates the tips using :py:meth:`~gratipay.models.team.Team.migrate_tips`. + + This function is wrapped in a script, ``bin/migrate-tips.py``, which is + `used during payday`_. + + .. _used during payday: http://inside.gratipay.com/howto/run-payday + + """ + teams = db.all(""" + SELECT distinct ON (t.id) t.*::teams + FROM teams t + JOIN tips ON t.owner = tips.tippee -- Only fetch teams whose owners had tips under Gratipay 1.0 + WHERE t.is_approved IS TRUE -- Only fetch approved teams + AND NOT EXISTS ( -- Make sure tips haven't been migrated for any teams with same owner + SELECT 1 + FROM payment_instructions pi + JOIN teams t2 ON t2.id = pi.team_id + WHERE t2.owner = t.owner + AND pi.ctime < t2.ctime + ) + """) + + for team in teams: + try: + ntips = team.migrate_tips() + print("Migrated {} tip(s) for '{}'".format(ntips, team.slug)) + except AlreadyMigrated: + print("'%s' already migrated." % team.slug) + + print("Done.") + + +class AlreadyMigrated(Exception): + """Raised by :py:meth:`~gratipay.models.team.migrate_tips`. + """ diff --git a/tests/py/test_tip_migration.py b/tests/py/test_tip_migration.py index fe1c53c0e3..3dcd1e11b4 100644 --- a/tests/py/test_tip_migration.py +++ b/tests/py/test_tip_migration.py @@ -2,7 +2,7 @@ import pytest from gratipay.testing import Harness -from gratipay.models.team import AlreadyMigrated, migrate_all_tips +from gratipay.models.team.mixins.tip_migration import AlreadyMigrated, migrate_all_tips class Tests(Harness):