From bbd25c745009b9d04bb74af43259e863f30ef658 Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Sat, 18 Mar 2023 10:48:07 +0000 Subject: [PATCH] Implement link groups --- app/urls.py | 22 +++ core/forms.py | 106 +++++++++++-- core/migrations/0030_linkgroup.py | 31 ++++ ...ggregators_remove_ad_platforms_and_more.py | 45 ++++++ core/models.py | 143 ++++++++++++------ core/templates/base.html | 5 +- core/templates/partials/aggregator-info.html | 13 ++ core/templates/partials/linkgroup-list.html | 85 +++++++++++ core/templates/partials/platform-trades.html | 2 +- core/templates/partials/wallet-list.html | 4 +- core/tests/test_platform.py | 17 ++- core/views/linkgroups.py | 36 +++++ core/views/wallets.py | 2 +- 13 files changed, 440 insertions(+), 71 deletions(-) create mode 100644 core/migrations/0030_linkgroup.py create mode 100644 core/migrations/0031_remove_ad_aggregators_remove_ad_platforms_and_more.py create mode 100644 core/templates/partials/linkgroup-list.html create mode 100644 core/views/linkgroups.py diff --git a/app/urls.py b/app/urls.py index 3f99600..9f19af7 100644 --- a/app/urls.py +++ b/app/urls.py @@ -25,6 +25,7 @@ from core.views import ( aggregators, banks, base, + linkgroups, notifications, platforms, profit, @@ -230,4 +231,25 @@ urlpatterns = [ wallets.WalletDelete.as_view(), name="wallet_delete", ), + # Link groups + path( + "links//", + linkgroups.LinkGroupList.as_view(), + name="linkgroups", + ), + path( + "links//create/", + linkgroups.LinkGroupCreate.as_view(), + name="linkgroup_create", + ), + path( + "links//update//", + linkgroups.LinkGroupUpdate.as_view(), + name="linkgroup_update", + ), + path( + "links//delete//", + linkgroups.LinkGroupDelete.as_view(), + name="linkgroup_delete", + ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/forms.py b/core/forms.py index af7ac22..7b7d0cf 100644 --- a/core/forms.py +++ b/core/forms.py @@ -8,6 +8,7 @@ from .models import ( Ad, Aggregator, Asset, + LinkGroup, NotificationSettings, Platform, Provider, @@ -73,6 +74,7 @@ class AggregatorForm(RestrictedFormMixin, ModelForm): "secret_id", "secret_key", "poll_interval", + "link_group", "enabled", ) help_texts = { @@ -81,6 +83,7 @@ class AggregatorForm(RestrictedFormMixin, ModelForm): "secret_id": "The secret ID for the aggregator service.", "secret_key": "The secret key for the aggregator service.", "poll_interval": "The interval in seconds to poll the aggregator service.", + "link_group": "The link group to use for this aggregator connection.", "enabled": "Whether or not the aggregator connection is enabled.", } @@ -120,6 +123,7 @@ class PlatformForm(RestrictedFormMixin, ModelForm): "base_usd", "withdrawal_trigger", "payees", + "link_group", "enabled", ) help_texts = { @@ -143,6 +147,7 @@ class PlatformForm(RestrictedFormMixin, ModelForm): "base_usd": "The amount in USD to keep in the platform.", "withdrawal_trigger": "The amount above the base USD to trigger a withdrawal.", "payees": "The wallet addresses to send profit concerning this platform to.", + "link_group": "The link group to use for this platform.", "enabled": "Whether or not the platform connection is enabled.", } @@ -169,11 +174,12 @@ class AdForm(RestrictedFormMixin, ModelForm): "dist_list", "asset_list", "provider_list", - "platforms", - "aggregators", + # "platforms", + # "aggregators", "account_whitelist", "send_reference", "visible", + "link_group", "enabled", ) help_texts = { @@ -185,11 +191,12 @@ class AdForm(RestrictedFormMixin, ModelForm): "dist_list": "Currency and country, space separated, one pair per line.", "asset_list": "List of assets to distribute ads for.", "provider_list": "List of providers to distribute ads for.", - "platforms": "Enabled platforms for this ad", - "aggregators": "Enabled aggregators for this ad", + # "platforms": "Enabled platforms for this ad", + # "aggregators": "Enabled aggregators for this ad", "account_whitelist": "List of account IDs to use, one per line.", "send_reference": "Whether or not to send the reference on new trades.", "visible": "Whether or not this ad is visible.", + "link_group": "The link group to use for this ad.", "enabled": "Whether or not this ad is enabled.", } @@ -205,18 +212,18 @@ class AdForm(RestrictedFormMixin, ModelForm): help_text=Meta.help_texts["provider_list"], required=True, ) - platforms = forms.ModelMultipleChoiceField( - queryset=Platform.objects.all(), - widget=forms.CheckboxSelectMultiple, - help_text=Meta.help_texts["platforms"], - required=True, - ) - aggregators = forms.ModelMultipleChoiceField( - queryset=Aggregator.objects.all(), - widget=forms.CheckboxSelectMultiple, - help_text=Meta.help_texts["aggregators"], - required=True, - ) + # platforms = forms.ModelMultipleChoiceField( + # queryset=Platform.objects.all(), + # widget=forms.CheckboxSelectMultiple, + # help_text=Meta.help_texts["platforms"], + # required=True, + # ) + # aggregators = forms.ModelMultipleChoiceField( + # queryset=Aggregator.objects.all(), + # widget=forms.CheckboxSelectMultiple, + # help_text=Meta.help_texts["aggregators"], + # required=True, + # ) class RequisitionForm(RestrictedFormMixin, ModelForm): @@ -254,3 +261,70 @@ class WalletForm(RestrictedFormMixin, ModelForm): "name": "The name of the wallet.", "address": "The XMR address to send funds to.", } + + +class LinkGroupForm(RestrictedFormMixin, ModelForm): + class Meta: + model = LinkGroup + fields = ( + "name", + "aggregators", + "platforms", + "platform_owner_cut_percentage", + "requisition_owner_cut_percentage", + "operator_cut_percentage", + "enabled", + ) + + help_texts = { + "name": "The name of the link group.", + "aggregators": "The aggregators to use.", + "platforms": "The platforms to use.", + "platform_owner_cut_percentage": "The percentage of the total profit of this group to give to the platform owners.", + "requisition_owner_cut_percentage": "The percentage of the total profit of this group to give to the requisition owners.", + "operator_cut_percentage": "The percentage of the total profit of this group to give to the operator.", + "enabled": "Whether or not this link group is enabled.", + } + + platforms = forms.ModelMultipleChoiceField( + queryset=Platform.objects.all(), + widget=forms.CheckboxSelectMultiple, + help_text=Meta.help_texts["platforms"], + required=True, + ) + aggregators = forms.ModelMultipleChoiceField( + queryset=Aggregator.objects.all(), + widget=forms.CheckboxSelectMultiple, + help_text=Meta.help_texts["aggregators"], + required=True, + ) + + def clean(self): + cleaned_data = super(LinkGroupForm, self).clean() + platform_owner_cut_percentage = cleaned_data.get( + "platform_owner_cut_percentage" + ) + requisition_owner_cut_percentage = cleaned_data.get( + "requisition_owner_cut_percentage" + ) + operator_cut_percentage = cleaned_data.get("operator_cut_percentage") + total_sum = ( + platform_owner_cut_percentage + + requisition_owner_cut_percentage + + operator_cut_percentage + ) + if total_sum != 100: + self.add_error( + "platform_owner_cut_percentage", + f"The sum of the percentages must be 100, not {total_sum}.", + ) + self.add_error( + "requisition_owner_cut_percentage", + f"The sum of the percentages must be 100, not {total_sum}.", + ) + self.add_error( + "operator_cut_percentage", + f"The sum of the percentages must be 100, not {total_sum}.", + ) + return + return cleaned_data diff --git a/core/migrations/0030_linkgroup.py b/core/migrations/0030_linkgroup.py new file mode 100644 index 0000000..407833f --- /dev/null +++ b/core/migrations/0030_linkgroup.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-03-18 10:12 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0029_alter_requisition_id_alter_wallet_id'), + ] + + operations = [ + migrations.CreateModel( + name='LinkGroup', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('platform_owner_cut_percentage', models.FloatField(default=0)), + ('requisition_owner_cut_percentage', models.FloatField(default=0)), + ('operator_cut_percentage', models.FloatField(default=0)), + ('enabled', models.BooleanField(default=True)), + ('aggregators', models.ManyToManyField(to='core.aggregator')), + ('platforms', models.ManyToManyField(to='core.platform')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0031_remove_ad_aggregators_remove_ad_platforms_and_more.py b/core/migrations/0031_remove_ad_aggregators_remove_ad_platforms_and_more.py new file mode 100644 index 0000000..9caee18 --- /dev/null +++ b/core/migrations/0031_remove_ad_aggregators_remove_ad_platforms_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.7 on 2023-03-18 10:38 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0030_linkgroup'), + ] + + operations = [ + migrations.RemoveField( + model_name='ad', + name='aggregators', + ), + migrations.RemoveField( + model_name='ad', + name='platforms', + ), + migrations.RemoveField( + model_name='linkgroup', + name='aggregators', + ), + migrations.RemoveField( + model_name='linkgroup', + name='platforms', + ), + migrations.AddField( + model_name='ad', + name='link_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'), + ), + migrations.AddField( + model_name='aggregator', + name='link_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'), + ), + migrations.AddField( + model_name='platform', + name='link_group', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.linkgroup'), + ), + ] diff --git a/core/models.py b/core/models.py index b09c154..31bd1b2 100644 --- a/core/models.py +++ b/core/models.py @@ -50,6 +50,26 @@ class NotificationSettings(models.Model): return f"Notification settings for {self.user}" +class LinkGroup(models.Model): + """ + A group linking Aggregators, Platforms and defining a percentage split + that the owners of each should receive. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + + platform_owner_cut_percentage = models.FloatField(default=0) + requisition_owner_cut_percentage = models.FloatField(default=0) + operator_cut_percentage = models.FloatField(default=0) + + enabled = models.BooleanField(default=True) + + def __str__(self): + return self.name + + class Aggregator(models.Model): """ A connection to an API aggregator to pull transactions from bank accounts. @@ -70,6 +90,10 @@ class Aggregator(models.Model): fetch_accounts = models.BooleanField(default=True) + link_group = models.ForeignKey( + LinkGroup, on_delete=models.CASCADE, null=True, blank=True + ) + enabled = models.BooleanField(default=True) def __str__(self): @@ -85,36 +109,41 @@ class Aggregator(models.Model): @classmethod def get_for_platform(cls, platform): - aggregators = [] - ads = Ad.objects.filter( - platforms=platform, - enabled=True, - ) - for ad in ads: - for aggregator in ad.aggregators.all(): - if aggregator not in aggregators: - aggregators.append(aggregator) + # aggregators = [] + # linkgroups = LinkGroup.objects.filter( + # platforms=platform, + # enabled=True, + # ) + # for link in linkgroups: + # for aggregator in link.aggregators.all(): + # if aggregator not in aggregators: + # aggregators.append(aggregator) + platform_link = platform.link_group - return aggregators + # return aggregators + return cls.objects.filter( + link_group=platform_link, + ) @property def platforms(self): """ Get platforms for this aggregator. - Do this by looking up Ads with the aggregator. + Do this by looking up LinkGroups with the aggregator. Then, join them all together. """ - platforms = [] - ads = Ad.objects.filter( - aggregators=self, - enabled=True, - ) - for ad in ads: - for platform in ad.platforms.all(): - if platform not in platforms: - platforms.append(platform) + return Platform.objects.filter(link_group=self.link_group) + # platforms = [] + # linkgroups = LinkGroup.objects.filter( + # aggregators=self, + # enabled=True, + # ) + # for link in linkgroups: + # for platform in link.platforms.all(): + # if platform not in platforms: + # platforms.append(platform) - return platforms + # return platforms @classmethod def get_currencies_for_platform(cls, platform): @@ -168,6 +197,9 @@ class Wallet(models.Model): name = models.CharField(max_length=255) address = models.CharField(max_length=255) + def __str__(self): + return self.name + class Platform(models.Model): """ @@ -205,6 +237,10 @@ class Platform(models.Model): payees = models.ManyToManyField(Wallet, blank=True) + link_group = models.ForeignKey( + LinkGroup, on_delete=models.CASCADE, null=True, blank=True + ) + enabled = models.BooleanField(default=True) def __str__(self): @@ -214,7 +250,9 @@ class Platform(models.Model): ad_id = self.platform_ad_ids.get(platform_ad_id, None) if not ad_id: return None - ad_object = Ad.objects.filter(id=ad_id, user=self.user, enabled=True).first() + ad_object = Ad.objects.filter( + id=ad_id, user=self.user, link_group=self.link_group, enabled=True + ).first() return ad_object @classmethod @@ -234,7 +272,9 @@ class Platform(models.Model): """ Get all ads linked to this platform. """ - return Ad.objects.filter(user=self.user, enabled=True, platforms=self) + return Ad.objects.filter( + user=self.user, enabled=True, link_group=self.link_group + ) @property def ads_assets(self): @@ -343,36 +383,45 @@ class Platform(models.Model): @classmethod def get_for_aggregator(cls, aggregator): - platforms = [] - ads = Ad.objects.filter( - aggregators=aggregator, - enabled=True, - ) - for ad in ads: - for platform in ad.platforms.all(): - if platform not in platforms: - platforms.append(platform) + # platforms = [] + # linkgroups = LinkGroup.objects.filter( + # aggregators=aggregator, + # enabled=True, + # ) + # for link in linkgroups: + # for platform in link.platforms.all(): + # if platform not in platforms: + # platforms.append(platform) - return platforms + # return platforms + + aggregator_link = aggregator.link_group + return cls.objects.filter( + link_group=aggregator_link, + ) @property def aggregators(self): """ Get aggregators for this platform. - Do this by looking up Ads with the platform. + Do this by looking up LinkGroups with the platform. Then, join them all together. """ - aggregators = [] - ads = Ad.objects.filter( - platforms=self, - enabled=True, - ) - for ad in ads: - for aggregator in ad.aggregators.all(): - if aggregator not in aggregators: - aggregators.append(aggregator) + # aggregators = [] + # linkgroups = LinkGroup.objects.filter( + # platforms=self, + # enabled=True, + # ) + # for link in linkgroups: + # for aggregator in link.aggregators.all(): + # if aggregator not in aggregators: + # aggregators.append(aggregator) - return aggregators + # return aggregators + + return Aggregator.objects.filter( + link_group=self.link_group, + ) def get_requisition(self, aggregator_id, requisition_id): """ @@ -425,15 +474,15 @@ class Ad(models.Model): asset_list = models.ManyToManyField(Asset) provider_list = models.ManyToManyField(Provider) - platforms = models.ManyToManyField(Platform) - aggregators = models.ManyToManyField(Aggregator) - account_map = models.JSONField(default=dict) account_whitelist = models.TextField(null=True, blank=True) send_reference = models.BooleanField(default=True) visible = models.BooleanField(default=True) + link_group = models.ForeignKey( + LinkGroup, on_delete=models.CASCADE, null=True, blank=True + ) enabled = models.BooleanField(default=True) @property diff --git a/core/templates/base.html b/core/templates/base.html index 2b64b91..3b24e5c 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -272,7 +272,10 @@ Platform Connections - Wallets + Profit Wallets + + + Link Groups diff --git a/core/templates/partials/aggregator-info.html b/core/templates/partials/aggregator-info.html index 61ccaef..cf7cac4 100644 --- a/core/templates/partials/aggregator-info.html +++ b/core/templates/partials/aggregator-info.html @@ -28,6 +28,19 @@ {{ item.accounts|length }}
+ + +
+ + + {% endfor %} + + +{# endcache #} \ No newline at end of file diff --git a/core/templates/partials/platform-trades.html b/core/templates/partials/platform-trades.html index 26b496f..b3e59ed 100644 --- a/core/templates/partials/platform-trades.html +++ b/core/templates/partials/platform-trades.html @@ -1,6 +1,6 @@ {% load cache %} {% load cachalot cache %} -{% get_last_invalidation 'core.Platform' as last %} +{% get_last_invalidation 'core.Trade' as last %} {% include 'mixins/partials/notify.html' %} {# cache 600 objects_platform_trades request.user.id object_list type last #} {% for platform_name, trade_map in object_list.items %} diff --git a/core/templates/partials/wallet-list.html b/core/templates/partials/wallet-list.html index a3a890f..132903e 100644 --- a/core/templates/partials/wallet-list.html +++ b/core/templates/partials/wallet-list.html @@ -1,8 +1,8 @@ {% load cache %} {% load cachalot cache %} -{% get_last_invalidation 'core.Ad' as last %} +{% get_last_invalidation 'core.Wallet' as last %} {% include 'mixins/partials/notify.html' %} -{# cache 600 objects_ads request.user.id object_list type last #} +{# cache 600 objects_wallets request.user.id object_list type last #}