Add asset groups and restrictions

This commit is contained in:
Mark Veidemanis 2023-02-10 14:33:17 +00:00
parent 7938bffc8d
commit f81d632df3
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
7 changed files with 230 additions and 32 deletions

View File

@ -239,23 +239,44 @@ urlpatterns = [
), ),
# Asset Groups # Asset Groups
path( path(
"assetgroup/<str:type>/", "group/<str:type>/",
assets.AssetGroupList.as_view(), assets.AssetGroupList.as_view(),
name="assetgroups", name="assetgroups",
), ),
path( path(
"assetgroup/<str:type>/create/", "group/<str:type>/create/",
assets.AssetGroupCreate.as_view(), assets.AssetGroupCreate.as_view(),
name="assetgroup_create", name="assetgroup_create",
), ),
path( path(
"assetgroup/<str:type>/update/<str:pk>/", "group/<str:type>/update/<str:pk>/",
assets.AssetGroupUpdate.as_view(), assets.AssetGroupUpdate.as_view(),
name="assetgroup_update", name="assetgroup_update",
), ),
path( path(
"assetgroup/<str:type>/delete/<str:pk>/", "group/<str:type>/delete/<str:pk>/",
assets.AssetGroupDelete.as_view(), assets.AssetGroupDelete.as_view(),
name="assetgroup_delete", name="assetgroup_delete",
), ),
# Asset Restrictions
path(
"restriction/<str:type>/<str:group>/",
assets.AssetRestrictionList.as_view(),
name="assetrestrictions",
),
path(
"restriction/<str:type>/create/<str:group>/",
assets.AssetRestrictionCreate.as_view(),
name="assetrestriction_create",
),
path(
"restriction/<str:type>/update/<str:group>/<str:pk>/",
assets.AssetRestrictionUpdate.as_view(),
name="assetrestriction_update",
),
path(
"restriction/<str:type>/delete/<str:group>/<str:pk>/",
assets.AssetRestrictionDelete.as_view(),
name="assetrestriction_delete",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -6,6 +6,7 @@ from django.forms import ModelForm
from .models import ( from .models import (
Account, Account,
AssetGroup, AssetGroup,
AssetRestriction,
Hook, Hook,
NotificationSettings, NotificationSettings,
RiskModel, RiskModel,
@ -342,3 +343,37 @@ class AssetGroupForm(RestrictedFormMixin, ModelForm):
"description": "Description of the asset group. Informational only.", "description": "Description of the asset group. Informational only.",
"account": "Account to pull assets from.", "account": "Account to pull assets from.",
} }
class AssetRestrictionForm(RestrictedFormMixin, ModelForm):
class Meta:
model = AssetRestriction
fields = (
"name",
"description",
"pairs",
)
help_texts = {
"name": "Name of the asset restriction group. Informational only.",
"description": "Description of the asset restriction group. Informational only.",
"pairs": "Comma-separated list of pairs to restrict.",
}
def clean(self):
cleaned_data = super(AssetRestrictionForm, self).clean()
if "pairs" in cleaned_data and cleaned_data["pairs"]:
new_pairs = []
pair_split = cleaned_data["pairs"].split(",")
if not pair_split:
self.add_error("pairs", "You must specify at least one pair.")
return
for pair in pair_split:
if pair:
new_pairs.append(pair.strip())
else:
self.add_error("pairs", f"You cannot have an empty pair: {pair}")
return
cleaned_data["pairs_parsed"] = new_pairs
return cleaned_data

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.4 on 2023-02-10 13:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0052_assetgroup_assetrestriction'),
]
operations = [
migrations.AddField(
model_name='assetrestriction',
name='pairs_parsed',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='assetgroup',
name='allowed',
field=models.JSONField(blank=True, default={}, null=True),
),
]

View File

@ -410,7 +410,7 @@ class AssetGroup(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE)
# Dict like {"RUB": True, "USD": False} # Dict like {"RUB": True, "USD": False}
allowed = models.JSONField(null=True, blank=True, default={}) allowed = models.JSONField(null=True, blank=True, default=dict)
def __str__(self): def __str__(self):
return self.name return self.name
@ -424,12 +424,20 @@ class AssetGroup(models.Model):
truthy_values = [x for x in self.allowed.values() if x is True] truthy_values = [x for x in self.allowed.values() if x is True]
return f"{len(truthy_values)}/{len(self.allowed)}" return f"{len(truthy_values)}/{len(self.allowed)}"
@property
def restrictions(self):
"""
Get the total number of restrictions for this group.
"""
return self.assetrestriction_set.count()
class AssetRestriction(models.Model): class AssetRestriction(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
pairs = models.CharField(max_length=4096, null=True, blank=True) pairs = models.CharField(max_length=4096, null=True, blank=True)
pairs_parsed = models.JSONField(null=True, blank=True)
group = models.ForeignKey( group = models.ForeignKey(
AssetGroup, on_delete=models.CASCADE, null=True, blank=True AssetGroup, on_delete=models.CASCADE, null=True, blank=True

View File

@ -14,6 +14,7 @@
<th>description</th> <th>description</th>
<th>account</th> <th>account</th>
<th>status</th> <th>status</th>
<th>restrictions</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in object_list %} {% for item in object_list %}
@ -24,6 +25,7 @@
<td>{{ item.description }}</td> <td>{{ item.description }}</td>
<td>{{ item.account }}</td> <td>{{ item.account }}</td>
<td>{{ item.matches }}</td> <td>{{ item.matches }}</td>
<td>{{ item.restrictions }}</td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button <button
@ -53,31 +55,15 @@
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} <a href="{% url 'assetrestrictions' type='page' group=item.id %}"><button
<a href="{# url 'account_info' type=type pk=item.id #}"><button class="button">
class="button"> <span class="icon-text">
<span class="icon-text"> <span class="icon">
<span class="icon"> <i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-eye"></i>
</span>
</span> </span>
</button> </span>
</a> </button>
{% else %} </a>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{# url 'account_info' 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-eye"></i>
</span>
</span>
</button>
{% endif %}
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,61 @@
{% include 'partials/notify.html' %}
<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>description</th>
<th>pairs</th>
<th>group</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.pairs_parsed|length }}</td>
<td>{{ item.group }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'assetrestriction_update' type=type group=item.group.id 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 'assetrestriction_delete' type=type group=item.group.id 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>

View File

@ -1,12 +1,14 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from core.forms import AssetGroupForm from core.forms import AssetGroupForm, AssetRestrictionForm
from core.models import AssetGroup from core.models import AssetGroup, AssetRestriction
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate from core.views import AbortSave, ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
# Asset Groups
class AssetGroupList(LoginRequiredMixin, ObjectList): class AssetGroupList(LoginRequiredMixin, ObjectList):
list_template = "partials/assetgroup-list.html" list_template = "partials/assetgroup-list.html"
@ -35,3 +37,65 @@ class AssetGroupUpdate(LoginRequiredMixin, ObjectUpdate):
class AssetGroupDelete(LoginRequiredMixin, ObjectDelete): class AssetGroupDelete(LoginRequiredMixin, ObjectDelete):
model = AssetGroup model = AssetGroup
# Asset Restrictions
class AssetRestrictionsPermissionMixin:
# Check the user has permission to view the asset group
# We have a user check on the AssetRestriction, but we need to check the
# AssetGroup as well
def set_extra_args(self, user):
self.extra_permission_args = {
"group__user": user,
"group__pk": self.kwargs["group"],
}
class AssetRestrictionList(
LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectList
):
list_template = "partials/assetrestriction-list.html"
model = AssetRestriction
page_title = "List of asset restrictions. Linked to asset groups."
list_url_name = "assetrestrictions"
list_url_args = ["type", "group"]
submit_url_name = "assetrestriction_create"
submit_url_args = ["type", "group"]
class AssetRestrictionCreate(
LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectCreate
):
model = AssetRestriction
form_class = AssetRestrictionForm
submit_url_name = "assetrestriction_create"
submit_url_args = ["type", "group"]
def pre_save_mutate(self, user, obj):
try:
assetgroup = AssetGroup.objects.get(pk=self.kwargs["group"], user=user)
obj.group = assetgroup
except AssetGroup.DoesNotExist:
log.error(f"Asset Group {self.kwargs['group']} does not exist")
raise AbortSave("asset group does not exist or you don't have access")
class AssetRestrictionUpdate(
LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectUpdate
):
model = AssetRestriction
form_class = AssetRestrictionForm
submit_url_name = "assetrestriction_update"
submit_url_args = ["type", "pk", "group"]
class AssetRestrictionDelete(
LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectDelete
):
model = AssetRestriction