diff --git a/app/dashboard/migrations/0034_data_for_old_grants.py b/app/dashboard/migrations/0034_data_for_old_grants.py new file mode 100644 index 00000000000..448e9b99d03 --- /dev/null +++ b/app/dashboard/migrations/0034_data_for_old_grants.py @@ -0,0 +1,136 @@ +# Generated by Django 2.1.7 on 2019-04-29 21:15 + +from django.db import migrations +from grants.models import Grant, Subscription +from dashboard.models import Activity +from datetime import datetime, timedelta +from pytz import UTC + +def record_grant_activity_helper(activity_type, grant, profile, date): + """Registers a new activity concerning a grant + + Args: + activity_type (str): The type of activity, as defined in dashboard.models.Activity. + grant (grants.models.Grant): The grant in question. + profile (dashboard.models.Profile): The current user's profile. + + """ + try: + grant_logo = grant.logo.url + except: + grant_logo = None + metadata = { + 'id': grant.id, + 'value_in_token': '{0:.2f}'.format(grant.amount_received), + 'amount_goal': '{0:.2f}'.format(grant.amount_goal), + 'token_name': grant.token_symbol, + 'title': grant.title, + 'grant_logo': grant_logo, + 'grant_url': grant.url, + 'category': 'grant', + } + kwargs = { + 'profile': profile, + 'grant': grant, + 'activity_type': activity_type, + 'metadata': metadata, + 'created_on': date, + } + if not activity_exists(activity_type, grant, 'grant', profile, date): + Activity.objects.create(**kwargs) + +def record_subscription_activity_helper(activity_type, subscription, profile, date): + """Registers a new activity concerning a grant subscription + + Args: + activity_type (str): The type of activity, as defined in dashboard.models.Activity. + subscription (grants.models.Subscription): The subscription in question. + profile (dashboard.models.Profile): The current user's profile. + + """ + try: + grant_logo = subscription.grant.logo.url + except: + grant_logo = None + metadata = { + 'id': subscription.id, + 'value_in_token': str(subscription.amount_per_period), + 'value_in_usdt_now': str(subscription.amount_per_period_usdt), + 'token_name': subscription.token_symbol, + 'title': subscription.grant.title, + 'grant_logo': grant_logo, + 'grant_url': subscription.grant.reference_url, + 'category': 'grant', + } + kwargs = { + 'profile': profile, + 'subscription': subscription, + 'activity_type': activity_type, + 'metadata': metadata, + 'created_on': date, + } + if not activity_exists(activity_type, subscription, 'subscription', profile, date): + Activity.objects.create(**kwargs) + +def close_enough(date1, date2): + bigger = max(date1, date2) + smaller = min(date1, date2) + delta = bigger - smaller + threshold = timedelta(1) # one day + return delta < threshold + +def activity_exists(activity_type, object, object_type, profile, date): + if object_type == 'grant': + candidates = Activity.objects.filter(grant = object) + else: + candidates = Activity.objects.filter(subscription__grant = object.grant) + for activity in candidates: + if activity.activity_type == activity_type: + if activity_type == 'new_grant' or activity_type == 'killed_grant': + return True # there can't be two activities of this type for one grant + elif activity.profile == profile and close_enough(activity.created_on, date): + return True + return False + +def generate_grant_activities(deployment): + for grant in Grant.objects.all(): + if grant.created_on < deployment: + profile = grant.admin_profile + date = grant.created_on + record_grant_activity_helper('new_grant', grant, profile, date) + if not(close_enough(grant.modified_on, grant.created_on)) and grant.modified_on < deployment: + date = grant.modified_on + if grant.active: + record_grant_activity_helper('update_grant', grant, profile, date) + else: + record_grant_activity_helper('killed_grant', grant, profile, date) + +def generate_subscription_activities(deployment): + for subscription in Subscription.objects.all(): + if subscription.created_on < deployment: + profile = subscription.contributor_profile + date = subscription.created_on + if subscription.num_tx_approved == 1: + record_subscription_activity_helper('new_grant_contribution', subscription, profile, date) + else: + record_subscription_activity_helper('new_grant_subscription', subscription, profile, date) + if not subscription.active and subscription.modified_on < deployment: + date = subscription.modified_on + record_subscription_activity_helper('killed_grant_contribution', subscription, profile, date) + +def generate_activities(apps, schema_editor): + deployment = datetime(2019, 4, 24, 11, 53, 00, tzinfo=UTC) + generate_grant_activities(deployment) + generate_subscription_activities(deployment) + +class Migration(migrations.Migration): + + + dependencies = [ + ('dashboard', '0033_bounty_bounty_categories'), + ('grants', '0024_auto_20190612_1645'), + ] + + operations = [ + migrations.RunPython(generate_activities), + ] diff --git a/app/dashboard/templates/shared/profile_activities_grant.html b/app/dashboard/templates/shared/profile_activities_grant.html index a1fcb06d04b..aae675c7ff3 100644 --- a/app/dashboard/templates/shared/profile_activities_grant.html +++ b/app/dashboard/templates/shared/profile_activities_grant.html @@ -21,7 +21,7 @@ {{ activity.grant.amount_received | floatformat:2 }} / {{ activity.grant.amount_goal | floatformat:2 }} {{ activity.grant.token_symbol }} - • {{ activity.created | naturaltime }} + • {{ activity.created_on | naturaltime }} diff --git a/app/dashboard/templates/shared/profile_activities_subscription.html b/app/dashboard/templates/shared/profile_activities_subscription.html index 0e0018badb1..9d34ad330a5 100644 --- a/app/dashboard/templates/shared/profile_activities_subscription.html +++ b/app/dashboard/templates/shared/profile_activities_subscription.html @@ -21,7 +21,7 @@ {{ activity.subscription.grant.amount_received | floatformat:2 }} / {{ activity.subscription.grant.amount_goal | floatformat:2 }} {{ activity.subscription.grant.token_symbol }} - • {{ activity.created | naturaltime }} + • {{ activity.created_on | naturaltime }} diff --git a/app/dashboard/tests/test_dashboard_migrations.py b/app/dashboard/tests/test_dashboard_migrations.py new file mode 100644 index 00000000000..1b9299568fe --- /dev/null +++ b/app/dashboard/tests/test_dashboard_migrations.py @@ -0,0 +1,321 @@ +from datetime import datetime +from decimal import Decimal +from importlib import import_module +from unittest import TestCase + +import pytest +import pytest_django +from dashboard.models import Activity, Profile +from grants.models import Grant, Subscription +from pytz import UTC + +grants = import_module('dashboard.migrations.0034_data_for_old_grants') + + +@pytest.fixture(autouse=True) +def start(): + pytest_django.plugin._blocking_manager.unblock() + Grant.objects.all().delete() + Subscription.objects.all().delete() + Activity.objects.all().delete() + yield + Profile.objects.all().delete() + +def test_close_enough(): + base_date = datetime(2019, 5, 27, 11, 6, 45, tzinfo=UTC) + near_date = datetime(2019, 5, 27, 18, 33, 21, tzinfo=UTC) + almost_one_exact_day_later = datetime(2019, 5, 28, 11, 6, 44, tzinfo=UTC) + one_exact_day_later = datetime(2019, 5, 28, 11, 6, 45, tzinfo=UTC) + far_date = datetime(2019, 5, 30, 19, 23, 51, tzinfo=UTC) + assert grants.close_enough(base_date, near_date) + assert grants.close_enough(near_date, base_date) + assert grants.close_enough(base_date, almost_one_exact_day_later) + assert grants.close_enough(almost_one_exact_day_later, base_date) + assert not(grants.close_enough(base_date, one_exact_day_later)) + assert not(grants.close_enough(one_exact_day_later, base_date)) + assert not(grants.close_enough(base_date, far_date)) + assert not(grants.close_enough(far_date, base_date)) + +@pytest.fixture +def profile(): + profile, created = Profile.objects.get_or_create( + data={}, + handle='e18r', + email='e18r@localhost' + ) + return profile + +@pytest.fixture +def new_grant(profile): + kwargs = { + 'created_on': datetime(2018, 12, 25, 14, 22, 35, tzinfo=UTC), + 'modified_on': datetime(2018, 12, 25, 14, 22, 55, tzinfo=UTC), + 'title': 'test 3', + 'description': 'test description', + 'reference_url': 'http://www.example.com', + 'admin_address': '0x8B04e71007A783B4965BaFE068EC062D935E93b5', + 'contract_owner_address': '0x8B04e71007A783B4965BaFE068EC062D935E93b5', + 'token_address': '0xFc1079D41D56D78e9FA2a857991F41D777104c74', + 'token_symbol': 'E18R', + 'amount_goal': Decimal('100.0000'), + 'contract_version': Decimal('0'), + 'deploy_tx_id': '0xa95d30415427f76c778207e789c78d436b5c4ca4339797cff52ed21de8419554', + 'network': 'rinkeby', + 'metadata': {}, + 'admin_profile': profile, + 'logo': None, + } + grant = Grant(**kwargs) + grant.save(update=False) + return grant + +@pytest.fixture +def new_modern_grant(new_grant): + new_grant.created_on = datetime(2019, 5, 27, 14, 12, 35, tzinfo=UTC) + new_grant.modified_on = datetime(2019, 5, 27, 14, 12, 55, tzinfo=UTC) + new_grant.save(update=False) + return new_grant + +@pytest.fixture +def new_grant_with_activity(new_grant, profile): + kwargs = { + 'created_on': datetime(2018, 12, 25, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'grant': new_grant, + 'activity_type': 'new_grant', + } + activity = Activity(**kwargs) + activity.save(update=False) + return new_grant + +@pytest.fixture +def updated_grant(new_grant): + new_grant.modified_on = datetime(2018, 12, 28, 14, 22, 35, tzinfo=UTC) + new_grant.save(update=False) + return new_grant + +@pytest.fixture +def updated_grant_with_activity(updated_grant, profile): + kwargs = { + 'created_on': datetime(2018, 12, 28, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'grant': updated_grant, + 'activity_type': 'update_grant', + } + activity = Activity(**kwargs) + activity.save(update=False) + return updated_grant + +@pytest.fixture +def killed_grant(updated_grant): + updated_grant.active = False + updated_grant.save(update=False) + return updated_grant + +@pytest.fixture +def killed_grant_with_activity(killed_grant, profile): + kwargs = { + 'created_on': datetime(2018, 12, 28, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'grant': killed_grant, + 'activity_type': 'killed_grant', + } + activity = Activity(**kwargs) + activity.save(update=False) + return killed_grant + +@pytest.fixture +def new_subscription(new_grant, profile): + kwargs = { + 'created_on': datetime(2018, 12, 29, 14, 22, 35, tzinfo=UTC), + 'active': False, + 'contributor_address': '0x8B04e71007A783B4965BaFE068EC062D935E93b5', + 'amount_per_period': 5, + 'real_period_seconds': 2592000, + 'frequency': 30, + 'frequency_unit': 'days', + 'token_address': '0xFc1079D41D56D78e9FA2a857991F41D777104c74', + 'token_symbol': 'E18R', + 'gas_price': 10, + 'new_approve_tx_id': '0xa95d30415427f76c778207e789c78d436b5c4ca4339797cff52ed21de8419554', + 'num_tx_approved': 12, + 'network': 'rinkeby', + 'contributor_profile': profile, + 'grant': new_grant, + 'last_contribution_date': datetime(2018, 1, 1, 15, 5, 25, tzinfo=UTC), + 'next_contribution_date': datetime(2020, 1, 1, 15, 5, 25, tzinfo=UTC), + } + return Subscription.objects.create(**kwargs) + +@pytest.fixture +def new_subscription_with_activity(new_subscription, profile): + kwargs = { + 'created_on': datetime(2018, 12, 29, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'subscription': new_subscription, + 'activity_type': 'new_grant_subscription', + } + activity = Activity(**kwargs) + activity.save(update=False) + return new_subscription + +@pytest.fixture +def new_contribution(new_subscription): + contribution = new_subscription + contribution.num_tx_approved = 1 + contribution.successful_contribution(contribution.new_approve_tx_id) + contribution.error = True + contribution.subminer_comments = "skipping" + contribution.save() + return contribution + +@pytest.fixture +def new_contribution_with_activity(new_contribution, profile): + kwargs = { + 'created_on': datetime(2018, 12, 29, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'subscription': new_contribution, + 'activity_type': 'new_grant_contribution', + } + activity = Activity(**kwargs) + activity.save(update=False) + return new_contribution + +@pytest.fixture +def cancelled_subscription(new_subscription): + new_subscription.end_approve_tx_id = '0xa95d30415427f76c778207e789c78d436b5c4ca4339797cff52ed21de8419554' + new_subscription.cancel_tx_id = '0xa95d30415427f76c778207e789c78d436b5c4ca4339797cff52ed21de8419554' + new_subscription.active = False + new_subscription.modified_on = datetime(2018, 12, 31, 14, 22, 35, tzinfo=UTC) + new_subscription.save(update=False) + return new_subscription + +@pytest.fixture +def cancelled_subscription_with_activity(cancelled_subscription, profile): + kwargs = { + 'created_on': datetime(2018, 12, 31, 14, 22, 35, tzinfo=UTC), + 'profile': profile, + 'subscription': cancelled_subscription, + 'activity_type': 'killed_grant_contribution', + } + activity = Activity(**kwargs) + activity.save(update=False) + return cancelled_subscription + +def test_new_grant(new_grant): + activities = Activity.objects.filter(grant = new_grant) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = new_grant) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant' + assert activity.created_on == new_grant.created_on + assert activity.profile == new_grant.admin_profile + +def test_update_grant(updated_grant): + activities = Activity.objects.filter(grant = updated_grant) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = updated_grant) + assert len(activities) == 2 + assert 'update_grant' in [a.activity_type for a in activities] + +def test_killed_grant(killed_grant): + activities = Activity.objects.filter(grant = killed_grant) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = killed_grant) + assert len(activities) == 2 # impossible to know if/when a killed grant was updated + assert 'killed_grant' in [a.activity_type for a in activities] + +def test_new_contribution(new_contribution): + activities = Activity.objects.filter(subscription = new_contribution) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = new_contribution) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant_contribution' + assert activity.created_on == new_contribution.created_on + assert activity.profile == new_contribution.contributor_profile + +def test_new_subscription(new_subscription): + activities = Activity.objects.filter(subscription = new_subscription) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = new_subscription) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant_subscription' + +def test_cancelled_subscription(cancelled_subscription): + activities = Activity.objects.filter(subscription = cancelled_subscription) + assert len(activities) == 0 + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = cancelled_subscription) + assert len(activities) == 2 + assert 'killed_grant_contribution' in [a.activity_type for a in activities] + +def test_avoid_duplicate_new_grant_activity(new_grant_with_activity): + activities = Activity.objects.filter(grant = new_grant_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant' + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = new_grant_with_activity) + assert len(activities) == 1 + +def test_avoid_duplicate_updated_grant_activity(updated_grant_with_activity): + activities = Activity.objects.filter(grant = updated_grant_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'update_grant' + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = updated_grant_with_activity) + assert len([a for a in activities if a.activity_type == 'update_grant']) == 1 + +def test_avoid_duplicate_killed_grant_activity(killed_grant_with_activity): + activities = Activity.objects.filter(grant = killed_grant_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'killed_grant' + grants.generate_activities(None, None) + activities = Activity.objects.filter(grant = killed_grant_with_activity) + assert len([a for a in activities if a.activity_type == 'killed_grant']) == 1 + +def test_avoid_duplicate_new_subscription_activity(new_subscription_with_activity): + activities = Activity.objects.filter( + subscription = new_subscription_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant_subscription' + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = new_subscription_with_activity) + assert len(activities) == 1 + +def test_avoid_duplicate_new_contribution_activity(new_contribution_with_activity): + activities = Activity.objects.filter( + subscription = new_contribution_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'new_grant_contribution' + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = new_contribution_with_activity) + assert len(activities) == 1 + +def test_avoid_duplicate_cancelled_subscription_activity(cancelled_subscription_with_activity): + activities = Activity.objects.filter( + subscription = cancelled_subscription_with_activity) + assert len(activities) == 1 + activity = activities[0] + assert activity.activity_type == 'killed_grant_contribution' + grants.generate_activities(None, None) + activities = Activity.objects.filter(subscription = cancelled_subscription_with_activity) + assert len([a for a in activities if a.activity_type == 'killed_grant_contribution']) == 1 + +def test_avoid_modern_grant_activity(new_modern_grant): + activities = Activity.objects.filter(grant = new_modern_grant) + assert len(activities) == 0 + grants.generate_activities(None, None) + assert len(activities) == 0 diff --git a/app/economy/models.py b/app/economy/models.py index a32bcf88d6f..48d9f6866dc 100644 --- a/app/economy/models.py +++ b/app/economy/models.py @@ -76,9 +76,10 @@ class Meta: created_on = models.DateTimeField(null=False, default=get_time, db_index=True) modified_on = models.DateTimeField(null=False, default=get_time) - def save(self, *args, **kwargs): - """Override the SuperModel save to handle modified_on logic.""" - self.modified_on = get_time() + def save(self, update=True, *args, **kwargs): + """Override the SuperModel save to optionally handle modified_on logic.""" + if update: + self.modified_on = get_time() return super(SuperModel, self).save(*args, **kwargs) def to_standard_dict(self, fields=None, exclude=None, properties=None):