Implement custom notification settings

This commit is contained in:
Mark Veidemanis 2022-12-18 17:21:52 +00:00
parent 4c463e88f2
commit 7ee698f457
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
9 changed files with 141 additions and 8 deletions

View File

@ -27,6 +27,7 @@ from core.views import (
callbacks, callbacks,
hooks, hooks,
limits, limits,
notifications,
positions, positions,
profit, profit,
signals, signals,
@ -208,4 +209,9 @@ urlpatterns = [
limits.TrendDirectionList.as_view(), limits.TrendDirectionList.as_view(),
name="trenddirections", name="trenddirections",
), ),
path(
"notifications/<str:type>/update/",
notifications.NotificationsUpdate.as_view(),
name="notifications_update",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -3,7 +3,16 @@ from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.forms import ModelForm from django.forms import ModelForm
from .models import Account, Hook, Signal, Strategy, Trade, TradingTime, User from .models import (
Account,
Hook,
NotificationSettings,
Signal,
Strategy,
Trade,
TradingTime,
User,
)
# flake8: noqa: E501 # flake8: noqa: E501
@ -276,3 +285,16 @@ class TradingTimeForm(RestrictedFormMixin, ModelForm):
"end_day": "The day of the week to stop trading.", "end_day": "The day of the week to stop trading.",
"end_time": "The time of day to stop trading.", "end_time": "The time of day to stop trading.",
} }
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = NotificationSettings
fields = (
"ntfy_topic",
"ntfy_url",
)
help_texts = {
"ntfy_topic": "The topic to send notifications to.",
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
}

View File

@ -592,12 +592,12 @@ def execute_strategy(callback, strategy, func):
new_trade.save() new_trade.save()
else: else:
info = new_trade.post() info = new_trade.post()
print("INFO", info)
log.debug(f"Posted trade: {info}") log.debug(f"Posted trade: {info}")
# Send notification with limited number of fields # Send notification with limited number of fields
wanted_fields = ["requestID", "type", "symbol", "units", "reason"] wanted_fields = ["requestID", "type", "symbol", "units", "reason"]
sendmsg( sendmsg(
user,
", ".join([str(v) for k, v in info.items() if k in wanted_fields]), ", ".join([str(v) for k, v in info.items() if k in wanted_fields]),
title=f"{direction} {amount_rounded} on {symbol}", title=f"{direction} {amount_rounded} on {symbol}",
) )

View File

@ -1,5 +1,4 @@
import requests import requests
from django.conf import settings
from core.util import logs from core.util import logs
@ -8,7 +7,8 @@ NTFY_URL = "https://ntfy.sh"
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
def sendmsg(msg, title=None, priority=None, tags=None): # Actual function to send a message to a topic
def _sendmsg(msg, title=None, priority=None, tags=None, url=None, topic=None):
headers = {"Title": "Fisk"} headers = {"Title": "Fisk"}
if title: if title:
headers["Title"] = title headers["Title"] = title
@ -17,7 +17,24 @@ def sendmsg(msg, title=None, priority=None, tags=None):
if tags: if tags:
headers["Tags"] = tags headers["Tags"] = tags
requests.post( requests.post(
f"{NTFY_URL}/{settings.NOTIFY_TOPIC}", f"{url}/{topic}",
data=msg, data=msg,
headers=headers, headers=headers,
) )
# Sendmsg helper to send a message to a user's notification settings
def sendmsg(user, *args, **kwargs):
notification_settings = user.get_notification_settings()
if notification_settings.ntfy_url is None:
url = NTFY_URL
else:
url = notification_settings.ntfy_url
if notification_settings.ntfy_topic is None:
# No topic set, so don't send
return
else:
topic = notification_settings.ntfy_topic
_sendmsg(*args, **kwargs, url=url, topic=topic)

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.3 on 2022-12-18 17:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0046_remove_hook_type_signal_type'),
]
operations = [
migrations.CreateModel(
name='NotificationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -97,6 +97,9 @@ class User(AbstractUser):
plan_list = [plan.name for plan in self.plans.all()] plan_list = [plan.name for plan in self.plans.all()]
return plan in plan_list return plan in plan_list
def get_notification_settings(self):
return NotificationSettings.objects.get_or_create(user=self)[0]
class Account(models.Model): class Account(models.Model):
EXCHANGE_CHOICES = (("alpaca", "Alpaca"), ("oanda", "OANDA")) EXCHANGE_CHOICES = (("alpaca", "Alpaca"), ("oanda", "OANDA"))
@ -363,6 +366,15 @@ class Strategy(models.Model):
return self.name return self.name
class NotificationSettings(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return f"Notification settings for {self.user}"
# class Perms(models.Model): # class Perms(models.Model):
# class Meta: # class Meta:
# permissions = ( # permissions = (

View File

@ -272,6 +272,20 @@
</a> </a>
</div> </div>
</div> </div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Account
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
Security
</a>
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
Notifications
</a>
</div>
</div>
{% endif %} {% endif %}
{% if settings.STRIPE_ENABLED %} {% if settings.STRIPE_ENABLED %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -298,7 +312,6 @@
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="button" href="{% url 'two_factor:profile' %}">Security</a>
<a class="button" href="{% url 'logout' %}">Logout</a> <a class="button" href="{% url 'logout' %}">Logout</a>
{% endif %} {% endif %}

View File

@ -343,9 +343,13 @@ class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView):
model = None model = None
submit_url_name = None submit_url_name = None
submit_url_args = ["type", "pk"]
request = None request = None
# Whether pk is required in the get request
pk_required = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.title = "Update " + self.context_object_name_singular self.title = "Update " + self.context_object_name_singular
@ -376,7 +380,8 @@ class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView):
if not type: if not type:
return HttpResponseBadRequest("No type specified") return HttpResponseBadRequest("No type specified")
if not pk: if not pk:
return HttpResponseBadRequest("No pk specified") if self.pk_required:
return HttpResponseBadRequest("No pk specified")
if type not in self.allowed_types: if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified") return HttpResponseBadRequest("Invalid type specified")
self.template_name = f"wm/{type}.html" self.template_name = f"wm/{type}.html"
@ -385,7 +390,15 @@ class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView):
type = "modal" type = "modal"
self.object = self.get_object() self.object = self.get_object()
submit_url = reverse(self.submit_url_name, kwargs={"type": type, "pk": pk})
submit_url_args = {}
for arg in self.submit_url_args:
if arg in locals():
submit_url_args[arg] = locals()[arg]
elif arg in kwargs:
submit_url_args[arg] = kwargs[arg]
submit_url = reverse(self.submit_url_name, kwargs=submit_url_args)
context = self.get_context_data() context = self.get_context_data()
form = kwargs.get("form", None) form = kwargs.get("form", None)
if form: if form:

View File

@ -0,0 +1,26 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from core.forms import NotificationSettingsForm
from core.models import NotificationSettings
from core.views import ObjectUpdate
# Notifications - we create a new notification settings object if there isn't one
# Hence, there is only an update view, not a create view.
class NotificationsUpdate(LoginRequiredMixin, ObjectUpdate):
model = NotificationSettings
form_class = NotificationSettingsForm
# list_url_name = "notifications"
# list_url_args = ["type"]
submit_url_name = "notifications_update"
submit_url_args = ["type"]
pk_required = False
def get_object(self, **kwargs):
notification_settings, _ = NotificationSettings.objects.get_or_create(
user=self.request.user
)
return notification_settings