diff --git a/app/urls.py b/app/urls.py index b8b161c..a8bc8a4 100644 --- a/app/urls.py +++ b/app/urls.py @@ -263,41 +263,25 @@ urlpatterns = [ assets.AssetGroupDelete.as_view(), name="assetgroup_delete", ), - # Asset Restrictions - # path( - # "restriction///", - # assets.AssetRestrictionList.as_view(), - # name="assetrestrictions", - # ), - # path( - # "restriction//create//", - # assets.AssetRestrictionCreate.as_view(), - # name="assetrestriction_create", - # ), - # path( - # "restriction//update///", - # assets.AssetRestrictionUpdate.as_view(), - # name="assetrestriction_update", - # ), - # path( - # "restriction//delete///", - # assets.AssetRestrictionDelete.as_view(), - # name="assetrestriction_delete", - # ), - # Asset group filters + # Asset Rules path( - "assetfilter//flip//", - assets.AssetFilterFlip.as_view(), - name="assetfilter_flip", + "assetrule///", + assets.AssetRuleList.as_view(), + name="assetrules", ), path( - "assetfilter//delete//", - assets.AssetFilterDelete.as_view(), - name="assetfilter_delete", + "assetrule//create//", + assets.AssetRuleCreate.as_view(), + name="assetrule_create", ), path( - "assetfilter//view//", - assets.AssetFilterList.as_view(), - name="assetfilters", + "assetrule//update///", + assets.AssetRuleUpdate.as_view(), + name="assetrule_update", + ), + path( + "assetrule//delete///", + assets.AssetRuleDelete.as_view(), + name="assetrule_delete", ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/forms.py b/core/forms.py index 3c2bb76..b0f9231 100644 --- a/core/forms.py +++ b/core/forms.py @@ -7,6 +7,7 @@ from mixins.restrictions import RestrictedFormMixin from .models import ( # AssetRestriction, Account, AssetGroup, + AssetRule, Hook, NotificationSettings, RiskModel, @@ -308,85 +309,38 @@ class AssetGroupForm(RestrictedFormMixin, ModelForm): fields = ( "name", "description", - "aggregation", - "trigger_below", - "trigger_above", + "when_no_data", + "when_no_match", + "when_no_aggregation", + "when_not_in_bounds", + "when_bullish", + "when_bearish", ) help_texts = { "name": "Name of the asset group. Informational only.", "description": "Description of the asset group. Informational only.", - "aggregation": "The aggregation method to use for this asset group.", - "trigger_below": "Trigger when the aggregation is below this value.", - "trigger_above": "Trigger when the aggregation is above this value.", + "when_no_data": "The action to take when no webhooks have been received for an asset.", + "when_no_match": "The action to take when there were no matches last callback for an asset.", + "when_no_aggregation": "The action to take when there is no defined aggregations for the asset.", + "when_not_in_bounds": "The action to take when the aggregation is not breaching either bound.", + "when_bullish": "The action to take when the asset is bullish.", + "when_bearish": "The action to take when the asset is bearish.", } - def clean(self): - cleaned_data = super(AssetGroupForm, self).clean() - if "aggregation" in cleaned_data: - if cleaned_data["aggregation"] == "none": - if "trigger_below" in cleaned_data and cleaned_data["trigger_below"]: - self.add_error( - "trigger_below", - "You cannot specify a trigger below value when aggregation is set to none.", - ) - return - if "trigger_above" in cleaned_data and cleaned_data["trigger_above"]: - self.add_error( - "trigger_above", - "You cannot specify a trigger above value when aggregation is set to none.", - ) - return - else: - # Check if either trigger_below or trigger_above has been set - if not any( - [cleaned_data["trigger_below"], cleaned_data["trigger_above"]] - ): - self.add_error( - "trigger_below", - "You must specify a trigger below and/or trigger above value when aggregation is set to anything other than none.", - ) - self.add_error( - "trigger_above", - "You must specify a trigger below and/or trigger above value when aggregation is set to anything other than none.", - ) - return - return cleaned_data +class AssetRuleForm(RestrictedFormMixin, ModelForm): + def __init__(self, *args, **kwargs): + super(AssetRuleForm, self).__init__(*args, **kwargs) + self.fields["value"].disabled = True + self.fields["aggregation"].disabled = True -# class AssetRestrictionForm(RestrictedFormMixin, ModelForm): -# class Meta: -# model = AssetRestriction -# fields = ( -# "name", -# "description", -# "pairs", -# "pairs_parsed", -# ) -# 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 when a webhook is received. This does nothing on its own.", -# } - -# pairs_parsed = forms.BooleanField(widget=forms.HiddenInput, required=False) - -# 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 -# else: -# cleaned_data["pairs_parsed"] = {} - -# return cleaned_data + class Meta: + model = AssetRule + fields = ( + "asset", + "aggregation", + "value", + "status", + "trigger_below", + "trigger_above", + ) diff --git a/core/migrations/0061_assetrule.py b/core/migrations/0061_assetrule.py new file mode 100644 index 0000000..24296c3 --- /dev/null +++ b/core/migrations/0061_assetrule.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.6 on 2023-02-13 18:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0060_assetgroup_aggregation_assetgroup_trigger_above_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AssetRule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('asset', models.CharField(max_length=64)), + ('aggregation', models.CharField(choices=[('none', 'None'), ('avg_sentiment', 'Average sentiment')], default='none', max_length=255)), + ('value', models.FloatField(blank=True, null=True)), + ('status', models.FloatField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Positive'), (3, 'Negative')], default=0)), + ('trigger_below', models.FloatField(blank=True, null=True)), + ('trigger_above', models.FloatField(blank=True, null=True)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.assetgroup')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0062_alter_assetrule_status.py b/core/migrations/0062_alter_assetrule_status.py new file mode 100644 index 0000000..66de078 --- /dev/null +++ b/core/migrations/0062_alter_assetrule_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.6 on 2023-02-13 18:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0061_assetrule'), + ] + + operations = [ + migrations.AlterField( + model_name='assetrule', + name='status', + field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Positive'), (3, 'Negative')], default=0), + ), + ] diff --git a/core/migrations/0063_alter_assetrule_unique_together.py b/core/migrations/0063_alter_assetrule_unique_together.py new file mode 100644 index 0000000..c33a9c4 --- /dev/null +++ b/core/migrations/0063_alter_assetrule_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.6 on 2023-02-13 19:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0062_alter_assetrule_status'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='assetrule', + unique_together={('asset', 'group')}, + ), + ] diff --git a/core/migrations/0064_remove_assetgroup_aggregation_and_more.py b/core/migrations/0064_remove_assetgroup_aggregation_and_more.py new file mode 100644 index 0000000..9b50c1c --- /dev/null +++ b/core/migrations/0064_remove_assetgroup_aggregation_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.6 on 2023-02-13 19:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0063_alter_assetrule_unique_together'), + ] + + operations = [ + migrations.RemoveField( + model_name='assetgroup', + name='aggregation', + ), + migrations.RemoveField( + model_name='assetgroup', + name='allowed', + ), + migrations.RemoveField( + model_name='assetgroup', + name='trigger_above', + ), + migrations.RemoveField( + model_name='assetgroup', + name='trigger_below', + ), + ] diff --git a/core/migrations/0065_assetgroup_when_bearish_assetgroup_when_bullish_and_more.py b/core/migrations/0065_assetgroup_when_bearish_assetgroup_when_bullish_and_more.py new file mode 100644 index 0000000..5783f95 --- /dev/null +++ b/core/migrations/0065_assetgroup_when_bearish_assetgroup_when_bullish_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.6 on 2023-02-13 20:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0064_remove_assetgroup_aggregation_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='assetgroup', + name='when_bearish', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetgroup', + name='when_bullish', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetgroup', + name='when_no_aggregation', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetgroup', + name='when_no_data', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetgroup', + name='when_no_match', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetgroup', + name='when_not_in_bounds', + field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255), + ), + migrations.AddField( + model_name='assetrule', + name='original_status', + field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'No action')], default=0), + ), + migrations.AlterField( + model_name='assetrule', + name='status', + field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'No action')], default=0), + ), + ] diff --git a/core/models.py b/core/models.py index 802e06d..901d737 100644 --- a/core/models.py +++ b/core/models.py @@ -46,6 +46,24 @@ AGGREGATION_CHOICES = ( ("avg_sentiment", "Average sentiment"), ) +STATUS_CHOICES = ( + (0, "No data"), + (1, "No match"), + (2, "Bullish"), + (3, "Bearish"), + (4, "No aggregation"), + (5, "Not in bounds"), + (6, "Always allow"), + (7, "Always deny"), +) + +MAPPING_CHOICES = ( + (6, "Always allow"), + (7, "Always deny"), + (2, "Bullish"), + (3, "Bearish"), +) + class Plan(models.Model): name = models.CharField(max_length=255, unique=True) @@ -416,16 +434,17 @@ class AssetGroup(models.Model): description = models.TextField(null=True, blank=True) # Dict like {"RUB": True, "USD": False} - allowed = models.JSONField(null=True, blank=True, default=dict) + # allowed = models.JSONField(null=True, blank=True, default=dict) webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - aggregation = models.CharField( - choices=AGGREGATION_CHOICES, max_length=255, default="none" - ) + when_no_data = models.IntegerField(choices=MAPPING_CHOICES, default=7) + when_no_match = models.IntegerField(choices=MAPPING_CHOICES, default=6) + when_no_aggregation = models.IntegerField(choices=MAPPING_CHOICES, default=6) + when_not_in_bounds = models.IntegerField(choices=MAPPING_CHOICES, default=6) - trigger_below = models.FloatField(null=True, blank=True) - trigger_above = models.FloatField(null=True, blank=True) + when_bullish = models.IntegerField(choices=MAPPING_CHOICES, default=2) + when_bearish = models.IntegerField(choices=MAPPING_CHOICES, default=3) def __str__(self): return f"{self.name}" @@ -435,20 +454,24 @@ class AssetGroup(models.Model): """ Get the total number of matches for this group. """ - if isinstance(self.allowed, dict): - truthy_values = [x for x in self.allowed.values() if x is True] - return f"{len(truthy_values)}/{len(self.allowed)}" + asset_rule_total = AssetRule.objects.filter(group=self).count() + asset_rule_positive = AssetRule.objects.filter(group=self, status=2).count() + return f"{asset_rule_positive}/{asset_rule_total}" -# class AssetRestriction(models.Model): -# user = models.ForeignKey(User, on_delete=models.CASCADE) -# name = models.CharField(max_length=255) -# description = models.TextField(null=True, blank=True) -# pairs = models.CharField(max_length=4096, null=True, blank=True) -# pairs_parsed = models.JSONField(null=True, blank=True, default=list) +class AssetRule(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + asset = models.CharField(max_length=64) + group = models.ForeignKey(AssetGroup, on_delete=models.CASCADE) + aggregation = models.CharField( + choices=AGGREGATION_CHOICES, max_length=255, default="none" + ) + value = models.FloatField(null=True, blank=True) + original_status = models.IntegerField(choices=STATUS_CHOICES, default=0) + status = models.IntegerField(choices=STATUS_CHOICES, default=0) + trigger_below = models.FloatField(null=True, blank=True) + trigger_above = models.FloatField(null=True, blank=True) -# webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - -# group = models.ForeignKey( -# AssetGroup, on_delete=models.CASCADE, null=True, blank=True -# ) + # Ensure that the asset is unique per group + class Meta: + unique_together = ("asset", "group") diff --git a/core/templates/partials/assetgroup-list.html b/core/templates/partials/assetgroup-list.html index 3d31ae2..732991c 100644 --- a/core/templates/partials/assetgroup-list.html +++ b/core/templates/partials/assetgroup-list.html @@ -1,6 +1,6 @@ {% load cache %} {% load cachalot cache %} -{% get_last_invalidation 'core.AssetGroup' as last %} +{% get_last_invalidation 'core.AssetGroup' 'core.AssetRule' as last %} {% include 'mixins/partials/notify.html' %} {% cache 600 objects_assetgroups request.user.id object_list last %} {{ item.description }} diff --git a/core/templates/partials/assetrestriction-list.html b/core/templates/partials/assetrule-list.html similarity index 61% rename from core/templates/partials/assetrestriction-list.html rename to core/templates/partials/assetrule-list.html index fa7a00a..353142d 100644 --- a/core/templates/partials/assetrestriction-list.html +++ b/core/templates/partials/assetrule-list.html @@ -1,8 +1,8 @@ {% load cache %} {% load cachalot cache %} -{% get_last_invalidation 'core.AssetRestriction' as last %} +{% get_last_invalidation 'core.AssetRule' as last %} {% include 'mixins/partials/notify.html' %} -{% cache 600 objects_assetrestrictions request.user.id object_list last %} +{% cache 600 objects_assetrules request.user.id object_list last %}
+ href="{% url 'assetrules' type='page' group=item.id %}"> {{ item.matches }}
- - - + - + + + + + + {% for item in object_list %} - + - - - + - + + + + + +
id usernamedescriptionpairsasset grouphookaggregationvalueoriginal statusstatustrigger abovetrigger below actions
{{ item.id }} {{ item.user }}{{ item.name }}{{ item.description }}{{ item.pairs_parsed|length }}{{ item.asset }} {{ item.group }} - - - - - - {{ item.get_aggregation_display }}{{ item.value }}{{ item.get_original_status_display }}{{ item.get_status_display }}{{ item.trigger_above }}{{ item.trigger_below }}