Implement link groups

This commit is contained in:
Mark Veidemanis 2023-03-18 10:48:07 +00:00
parent 0723f14c53
commit bbd25c7450
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
13 changed files with 440 additions and 71 deletions

View File

@ -25,6 +25,7 @@ from core.views import (
aggregators, aggregators,
banks, banks,
base, base,
linkgroups,
notifications, notifications,
platforms, platforms,
profit, profit,
@ -230,4 +231,25 @@ urlpatterns = [
wallets.WalletDelete.as_view(), wallets.WalletDelete.as_view(),
name="wallet_delete", name="wallet_delete",
), ),
# Link groups
path(
"links/<str:type>/",
linkgroups.LinkGroupList.as_view(),
name="linkgroups",
),
path(
"links/<str:type>/create/",
linkgroups.LinkGroupCreate.as_view(),
name="linkgroup_create",
),
path(
"links/<str:type>/update/<str:pk>/",
linkgroups.LinkGroupUpdate.as_view(),
name="linkgroup_update",
),
path(
"links/<str:type>/delete/<str:pk>/",
linkgroups.LinkGroupDelete.as_view(),
name="linkgroup_delete",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -8,6 +8,7 @@ from .models import (
Ad, Ad,
Aggregator, Aggregator,
Asset, Asset,
LinkGroup,
NotificationSettings, NotificationSettings,
Platform, Platform,
Provider, Provider,
@ -73,6 +74,7 @@ class AggregatorForm(RestrictedFormMixin, ModelForm):
"secret_id", "secret_id",
"secret_key", "secret_key",
"poll_interval", "poll_interval",
"link_group",
"enabled", "enabled",
) )
help_texts = { help_texts = {
@ -81,6 +83,7 @@ class AggregatorForm(RestrictedFormMixin, ModelForm):
"secret_id": "The secret ID for the aggregator service.", "secret_id": "The secret ID for the aggregator service.",
"secret_key": "The secret key for the aggregator service.", "secret_key": "The secret key for the aggregator service.",
"poll_interval": "The interval in seconds to poll 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.", "enabled": "Whether or not the aggregator connection is enabled.",
} }
@ -120,6 +123,7 @@ class PlatformForm(RestrictedFormMixin, ModelForm):
"base_usd", "base_usd",
"withdrawal_trigger", "withdrawal_trigger",
"payees", "payees",
"link_group",
"enabled", "enabled",
) )
help_texts = { help_texts = {
@ -143,6 +147,7 @@ class PlatformForm(RestrictedFormMixin, ModelForm):
"base_usd": "The amount in USD to keep in the platform.", "base_usd": "The amount in USD to keep in the platform.",
"withdrawal_trigger": "The amount above the base USD to trigger a withdrawal.", "withdrawal_trigger": "The amount above the base USD to trigger a withdrawal.",
"payees": "The wallet addresses to send profit concerning this platform to.", "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.", "enabled": "Whether or not the platform connection is enabled.",
} }
@ -169,11 +174,12 @@ class AdForm(RestrictedFormMixin, ModelForm):
"dist_list", "dist_list",
"asset_list", "asset_list",
"provider_list", "provider_list",
"platforms", # "platforms",
"aggregators", # "aggregators",
"account_whitelist", "account_whitelist",
"send_reference", "send_reference",
"visible", "visible",
"link_group",
"enabled", "enabled",
) )
help_texts = { help_texts = {
@ -185,11 +191,12 @@ class AdForm(RestrictedFormMixin, ModelForm):
"dist_list": "Currency and country, space separated, one pair per line.", "dist_list": "Currency and country, space separated, one pair per line.",
"asset_list": "List of assets to distribute ads for.", "asset_list": "List of assets to distribute ads for.",
"provider_list": "List of providers to distribute ads for.", "provider_list": "List of providers to distribute ads for.",
"platforms": "Enabled platforms for this ad", # "platforms": "Enabled platforms for this ad",
"aggregators": "Enabled aggregators for this ad", # "aggregators": "Enabled aggregators for this ad",
"account_whitelist": "List of account IDs to use, one per line.", "account_whitelist": "List of account IDs to use, one per line.",
"send_reference": "Whether or not to send the reference on new trades.", "send_reference": "Whether or not to send the reference on new trades.",
"visible": "Whether or not this ad is visible.", "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.", "enabled": "Whether or not this ad is enabled.",
} }
@ -205,18 +212,18 @@ class AdForm(RestrictedFormMixin, ModelForm):
help_text=Meta.help_texts["provider_list"], help_text=Meta.help_texts["provider_list"],
required=True, required=True,
) )
platforms = forms.ModelMultipleChoiceField( # platforms = forms.ModelMultipleChoiceField(
queryset=Platform.objects.all(), # queryset=Platform.objects.all(),
widget=forms.CheckboxSelectMultiple, # widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["platforms"], # help_text=Meta.help_texts["platforms"],
required=True, # required=True,
) # )
aggregators = forms.ModelMultipleChoiceField( # aggregators = forms.ModelMultipleChoiceField(
queryset=Aggregator.objects.all(), # queryset=Aggregator.objects.all(),
widget=forms.CheckboxSelectMultiple, # widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["aggregators"], # help_text=Meta.help_texts["aggregators"],
required=True, # required=True,
) # )
class RequisitionForm(RestrictedFormMixin, ModelForm): class RequisitionForm(RestrictedFormMixin, ModelForm):
@ -254,3 +261,70 @@ class WalletForm(RestrictedFormMixin, ModelForm):
"name": "The name of the wallet.", "name": "The name of the wallet.",
"address": "The XMR address to send funds to.", "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

View File

@ -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)),
],
),
]

View File

@ -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'),
),
]

View File

@ -50,6 +50,26 @@ class NotificationSettings(models.Model):
return f"Notification settings for {self.user}" 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): class Aggregator(models.Model):
""" """
A connection to an API aggregator to pull transactions from bank accounts. 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) fetch_accounts = models.BooleanField(default=True)
link_group = models.ForeignKey(
LinkGroup, on_delete=models.CASCADE, null=True, blank=True
)
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
def __str__(self): def __str__(self):
@ -85,36 +109,41 @@ class Aggregator(models.Model):
@classmethod @classmethod
def get_for_platform(cls, platform): def get_for_platform(cls, platform):
aggregators = [] # aggregators = []
ads = Ad.objects.filter( # linkgroups = LinkGroup.objects.filter(
platforms=platform, # platforms=platform,
enabled=True, # enabled=True,
) # )
for ad in ads: # for link in linkgroups:
for aggregator in ad.aggregators.all(): # for aggregator in link.aggregators.all():
if aggregator not in aggregators: # if aggregator not in aggregators:
aggregators.append(aggregator) # aggregators.append(aggregator)
platform_link = platform.link_group
return aggregators # return aggregators
return cls.objects.filter(
link_group=platform_link,
)
@property @property
def platforms(self): def platforms(self):
""" """
Get platforms for this aggregator. 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. Then, join them all together.
""" """
platforms = [] return Platform.objects.filter(link_group=self.link_group)
ads = Ad.objects.filter( # platforms = []
aggregators=self, # linkgroups = LinkGroup.objects.filter(
enabled=True, # aggregators=self,
) # enabled=True,
for ad in ads: # )
for platform in ad.platforms.all(): # for link in linkgroups:
if platform not in platforms: # for platform in link.platforms.all():
platforms.append(platform) # if platform not in platforms:
# platforms.append(platform)
return platforms # return platforms
@classmethod @classmethod
def get_currencies_for_platform(cls, platform): def get_currencies_for_platform(cls, platform):
@ -168,6 +197,9 @@ class Wallet(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
address = models.CharField(max_length=255) address = models.CharField(max_length=255)
def __str__(self):
return self.name
class Platform(models.Model): class Platform(models.Model):
""" """
@ -205,6 +237,10 @@ class Platform(models.Model):
payees = models.ManyToManyField(Wallet, blank=True) payees = models.ManyToManyField(Wallet, blank=True)
link_group = models.ForeignKey(
LinkGroup, on_delete=models.CASCADE, null=True, blank=True
)
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
def __str__(self): def __str__(self):
@ -214,7 +250,9 @@ class Platform(models.Model):
ad_id = self.platform_ad_ids.get(platform_ad_id, None) ad_id = self.platform_ad_ids.get(platform_ad_id, None)
if not ad_id: if not ad_id:
return None 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 return ad_object
@classmethod @classmethod
@ -234,7 +272,9 @@ class Platform(models.Model):
""" """
Get all ads linked to this platform. 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 @property
def ads_assets(self): def ads_assets(self):
@ -343,36 +383,45 @@ class Platform(models.Model):
@classmethod @classmethod
def get_for_aggregator(cls, aggregator): def get_for_aggregator(cls, aggregator):
platforms = [] # platforms = []
ads = Ad.objects.filter( # linkgroups = LinkGroup.objects.filter(
aggregators=aggregator, # aggregators=aggregator,
enabled=True, # enabled=True,
) # )
for ad in ads: # for link in linkgroups:
for platform in ad.platforms.all(): # for platform in link.platforms.all():
if platform not in platforms: # if platform not in platforms:
platforms.append(platform) # platforms.append(platform)
return platforms # return platforms
aggregator_link = aggregator.link_group
return cls.objects.filter(
link_group=aggregator_link,
)
@property @property
def aggregators(self): def aggregators(self):
""" """
Get aggregators for this platform. 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. Then, join them all together.
""" """
aggregators = [] # aggregators = []
ads = Ad.objects.filter( # linkgroups = LinkGroup.objects.filter(
platforms=self, # platforms=self,
enabled=True, # enabled=True,
) # )
for ad in ads: # for link in linkgroups:
for aggregator in ad.aggregators.all(): # for aggregator in link.aggregators.all():
if aggregator not in aggregators: # if aggregator not in aggregators:
aggregators.append(aggregator) # aggregators.append(aggregator)
return aggregators # return aggregators
return Aggregator.objects.filter(
link_group=self.link_group,
)
def get_requisition(self, aggregator_id, requisition_id): def get_requisition(self, aggregator_id, requisition_id):
""" """
@ -425,15 +474,15 @@ class Ad(models.Model):
asset_list = models.ManyToManyField(Asset) asset_list = models.ManyToManyField(Asset)
provider_list = models.ManyToManyField(Provider) provider_list = models.ManyToManyField(Provider)
platforms = models.ManyToManyField(Platform)
aggregators = models.ManyToManyField(Aggregator)
account_map = models.JSONField(default=dict) account_map = models.JSONField(default=dict)
account_whitelist = models.TextField(null=True, blank=True) account_whitelist = models.TextField(null=True, blank=True)
send_reference = models.BooleanField(default=True) send_reference = models.BooleanField(default=True)
visible = 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) enabled = models.BooleanField(default=True)
@property @property

View File

@ -272,7 +272,10 @@
Platform Connections Platform Connections
</a> </a>
<a class="navbar-item" href="{% url 'wallets' type='page' %}"> <a class="navbar-item" href="{% url 'wallets' type='page' %}">
Wallets Profit Wallets
</a>
<a class="navbar-item" href="{% url 'linkgroups' type='page' %}">
Link Groups
</a> </a>
</div> </div>
</div> </div>

View File

@ -28,6 +28,19 @@
<td>{{ item.accounts|length }}</td> <td>{{ item.accounts|length }}</td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'requisition_update' type=type aggregator_id=pk req_id=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon has-text-black" data-tooltip="Configure">
<i class="fa-solid fa-wrench"></i>
</span>
</span>
</button>
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'req_delete' type=type pk=pk req_id=item.id %}" hx-delete="{% url 'req_delete' type=type pk=pk req_id=item.id %}"

View File

@ -0,0 +1,85 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.LinkGroup' as last %}
{% include 'mixins/partials/notify.html' %}
{# cache 600 objects_linkgroups request.user.id object_list type last #}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>platform</th>
<th>requisition</th>
<th>operator</th>
<th>enabled</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>
<a
class="has-text-grey"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.id }}');">
<span class="icon" data-tooltip="Copy to clipboard">
<i class="fa-solid fa-copy" aria-hidden="true"></i>
</span>
</a>
</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.platform_owner_cut_percentage }}</td>
<td>{{ item.requisition_owner_cut_percentage }}</td>
<td>{{ item.operator_cut_percentage }}</td>
<td>
{% if item.enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'linkgroup_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'linkgroup_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{# endcache #}

View File

@ -1,6 +1,6 @@
{% load cache %} {% load cache %}
{% load cachalot cache %} {% load cachalot cache %}
{% get_last_invalidation 'core.Platform' as last %} {% get_last_invalidation 'core.Trade' as last %}
{% include 'mixins/partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{# cache 600 objects_platform_trades request.user.id object_list type last #} {# cache 600 objects_platform_trades request.user.id object_list type last #}
{% for platform_name, trade_map in object_list.items %} {% for platform_name, trade_map in object_list.items %}

View File

@ -1,8 +1,8 @@
{% load cache %} {% load cache %}
{% load cachalot cache %} {% load cachalot cache %}
{% get_last_invalidation 'core.Ad' as last %} {% get_last_invalidation 'core.Wallet' as last %}
{% include 'mixins/partials/notify.html' %} {% 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 #}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"

View File

@ -1,7 +1,7 @@
from django.test import TransactionTestCase from django.test import TransactionTestCase
from core.clients.platform import LocalPlatformClient from core.clients.platform import LocalPlatformClient
from core.models import Ad, Asset, Provider, Requisition from core.models import Ad, Asset, LinkGroup, Provider, Requisition
from core.tests.helpers import AggregatorPlatformMixin from core.tests.helpers import AggregatorPlatformMixin
@ -93,11 +93,22 @@ class TestPlatform(AggregatorPlatformMixin, TransactionTestCase):
self.ad.asset_list.set([asset]) self.ad.asset_list.set([asset])
self.ad.provider_list.set([provider]) self.ad.provider_list.set([provider])
self.ad.platforms.set([self.platform]) # self.ad.platforms.set([self.platform])
self.ad.aggregators.set([self.aggregator]) # self.ad.aggregators.set([self.aggregator])
self.ad.save() self.ad.save()
self.linkgroup = LinkGroup.objects.create(
user=self.user,
name="Test",
)
self.aggregator.link_group = self.linkgroup
self.aggregator.save()
self.platform.link_group = self.linkgroup
self.platform.save()
self.req = Requisition.objects.create( self.req = Requisition.objects.create(
user=self.user, user=self.user,
aggregator=self.aggregator, aggregator=self.aggregator,

36
core/views/linkgroups.py Normal file
View File

@ -0,0 +1,36 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from two_factor.views.mixins import OTPRequiredMixin
from core.forms import LinkGroupForm
from core.models import LinkGroup
class LinkGroupList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/linkgroup-list.html"
model = LinkGroup
page_title = "List of link groups"
page_subtitle = "Link groups are used to link aggregators and platforms"
list_url_name = "linkgroups"
list_url_args = ["type"]
submit_url_name = "linkgroup_create"
class LinkGroupCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
model = LinkGroup
form_class = LinkGroupForm
submit_url_name = "linkgroup_create"
class LinkGroupUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
model = LinkGroup
form_class = LinkGroupForm
submit_url_name = "linkgroup_update"
class LinkGroupDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = LinkGroup

View File

@ -9,7 +9,7 @@ from core.models import Wallet
class WalletList(LoginRequiredMixin, OTPRequiredMixin, ObjectList): class WalletList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/wallet-list.html" list_template = "partials/wallet-list.html"
model = Wallet model = Wallet
page_title = "List of wallets" page_title = "List of wallets to send profit to"
list_url_name = "wallets" list_url_name = "wallets"
list_url_args = ["type"] list_url_args = ["type"]