Implement CRUD for accounts and trades

This commit is contained in:
Mark Veidemanis 2022-10-17 18:56:16 +01:00
parent 7779cb8d0e
commit 2bafdf0910
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
18 changed files with 802 additions and 12 deletions

View File

@ -21,7 +21,7 @@ from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp.forms import OTPAuthenticationForm from django_otp.forms import OTPAuthenticationForm
from core.views import base, callbacks, hooks from core.views import accounts, base, callbacks, hooks, trades
from core.views.stripe_callbacks import Callback from core.views.stripe_callbacks import Callback
urlpatterns = [ urlpatterns = [
@ -72,4 +72,47 @@ urlpatterns = [
name="callbacks", name="callbacks",
), ),
path("callbacks/<str:type>/", callbacks.Callbacks.as_view(), name="callbacks"), path("callbacks/<str:type>/", callbacks.Callbacks.as_view(), name="callbacks"),
path("accounts/<str:type>/", accounts.Accounts.as_view(), name="accounts"),
path(
"accounts/<str:type>/add/",
accounts.AccountAction.as_view(),
name="account_action",
),
path(
"accounts/<str:type>/add/<str:name>/",
accounts.AccountAction.as_view(),
name="account_action",
),
path(
"accounts/<str:type>/del/<str:account_id>/",
accounts.AccountAction.as_view(),
name="account_action",
),
path(
"accounts/<str:type>/edit/<str:account_id>/",
accounts.AccountAction.as_view(),
name="account_action",
),
path("trades/<str:type>/", trades.Trades.as_view(), name="trades"),
path("trades/<str:type>/add/", trades.TradeAction.as_view(), name="trade_action"),
path(
"trades/<str:type>/<str:account_id>/",
trades.Trades.as_view(),
name="trades",
),
path(
"trades/<str:type>/add/<str:name>/",
trades.TradeAction.as_view(),
name="trade_action",
),
path(
"trades/<str:type>/del/<str:trade_id>/",
trades.TradeAction.as_view(),
name="trade_action",
),
path(
"trades/<str:type>/edit/<str:trade_id>/",
trades.TradeAction.as_view(),
name="trade_action",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.forms import ModelForm from django.forms import ModelForm
from .models import Hook, User from .models import Account, Hook, Trade, User
# Create your forms here. # Create your forms here.
@ -42,3 +42,27 @@ class HookForm(ModelForm):
"name", "name",
"hook", "hook",
) )
class AccountForm(ModelForm):
class Meta:
model = Account
fields = (
"exchange",
"api_key",
"api_secret",
)
class TradeForm(ModelForm):
class Meta:
model = Trade
fields = (
"account",
"symbol",
"type",
"amount",
"price",
"stop_loss",
"take_profit",
)

View File

@ -0,0 +1,26 @@
# Generated by Django 4.1.2 on 2022-10-17 17:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0006_remove_callback_market_alter_callback_timestamp_sent_and_more'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('exchange', models.CharField(max_length=255)),
('api_key', models.CharField(max_length=255)),
('api_secret', models.CharField(max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.1.2 on 2022-10-17 17:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0007_account'),
]
operations = [
migrations.CreateModel(
name='Trade',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('amount', models.FloatField()),
('price', models.FloatField()),
('stop_loss', models.FloatField(blank=True, null=True)),
('take_profit', models.FloatField(blank=True, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
],
),
]

View File

@ -66,6 +66,14 @@ class User(AbstractUser):
return plan in plan_list return plan in plan_list
class Account(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
exchange = models.CharField(max_length=255)
api_key = models.CharField(max_length=255)
api_secret = models.CharField(max_length=255)
class Session(models.Model): class Session(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
request = models.CharField(max_length=255, null=True, blank=True) request = models.CharField(max_length=255, null=True, blank=True)
@ -81,6 +89,17 @@ class Hook(models.Model):
received = models.IntegerField(default=0) received = models.IntegerField(default=0)
class Trade(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
symbol = models.CharField(max_length=255)
type = models.CharField(max_length=255)
amount = models.FloatField()
price = models.FloatField()
stop_loss = models.FloatField(null=True, blank=True)
take_profit = models.FloatField(null=True, blank=True)
class Callback(models.Model): class Callback(models.Model):
hook = models.ForeignKey(Hook, on_delete=models.CASCADE) hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
title = models.CharField(max_length=1024, null=True, blank=True) title = models.CharField(max_length=1024, null=True, blank=True)

View File

@ -201,6 +201,16 @@
<a class="navbar-item" href="{% url 'home' %}"> <a class="navbar-item" href="{% url 'home' %}">
Home Home
</a> </a>
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'trades' type='page' %}">
Trades
</a>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'hooks' type='page' %}"> <a class="navbar-item" href="{% url 'hooks' type='page' %}">
Hooks Hooks

View File

@ -4,7 +4,7 @@
{% block outer_content %} {% block outer_content %}
<div class="grid-stack" id="grid-stack-main"> <div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="7" gs-h="15" gs-y="0" gs-x="1"> <div class="grid-stack-item" gs-w="7" gs-h="25" gs-y="0" gs-x="1">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">

View File

@ -0,0 +1,74 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="accounts-table">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>exchange</th>
<th>API key</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.api_jey }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'account_action' type=type account_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-info">
<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 'account_action' type=type account_id=item.id %}"
hx-trigger="click"
hx-target="#accounts-table"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'trades' type=type account_id=item.id %}"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trades' type=type account_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,80 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="trades-table">
<thead>
<th>id</th>
<th>account id</th>
<th>symbol</th>
<th>type</th>
<th>amount</th>
<th>price</th>
<th>SL</th>
<th>TL</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.account.id }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.type }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.price }}</td>
<td>{{ item.stop_loss }}</td>
<td>{{ item.take_profit }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type trade_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-info">
<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 'trade_action' type=type trade_id=item.id %}"
hx-trigger="click"
hx-target="#trades-table"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="#"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,20 @@
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'account_action' type=type %}"
hx-trigger="click"
hx-target="#modals-here"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-plus"></i>
</span>
<span>Account</span>
</span>
</button>
</div>
{% include 'partials/notify.html' %}
{% include 'partials/account-list.html' %}

View File

@ -0,0 +1,31 @@
{% include 'partials/notify.html' %}
{% load crispy_forms_tags %}
{% load crispy_forms_bulma_field %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
{% if account_id is not None %}
hx-put="{% url 'account_action' type=type account_id=account_id %}"
{% else %}
hx-put="{% url 'account_action' type=type %}"
{% endif %}
hx-target="#accounts-table"
hx-swap="outerHTML">
{% csrf_token %}
{{ form|crispy }}
<button
type="button"
class="button is-light modal-close-button">
Cancel
</button>
<button type="submit" class="button is-info modal-close-button">Submit</button>
</form>

View File

@ -0,0 +1,31 @@
{% include 'partials/notify.html' %}
{% load crispy_forms_tags %}
{% load crispy_forms_bulma_field %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
{% if trade_id is not None %}
hx-put="{% url 'trade_action' type=type trade_id=trade_id %}"
{% else %}
hx-put="{% url 'trade_action' type=type %}"
{% endif %}
hx-target="#trades-table"
hx-swap="outerHTML">
{% csrf_token %}
{{ form|crispy }}
<button
type="button"
class="button is-light modal-close-button">
Cancel
</button>
<button type="submit" class="button is-info modal-close-button">Submit</button>
</form>

View File

@ -90,6 +90,47 @@
</button> </button>
<td> <td>
</tr> </tr>
</div> <tr>
<td>Accounts</td>
</table> <td>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'accounts' type='modal' %}"
hx-trigger="click"
hx-target="#modals-here"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Modal">
<i class="fa-solid fa-window-maximize"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'accounts' type='widget' %}"
hx-trigger="click"
hx-target="#widgets-here"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Widget">
<i class="fa-solid fa-sidebar"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'accounts' type='window' %}"
hx-trigger="click"
hx-target="#windows-here"
hx-swap="afterend"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Window">
<i class="fa-solid fa-window-restore"></i>
</span>
</span>
</button>
<td>
</tr>
</div>
</table>

View File

@ -0,0 +1,20 @@
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type %}"
hx-trigger="click"
hx-target="#modals-here"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-plus"></i>
</span>
<span>Trade</span>
</span>
</button>
</div>
{% include 'partials/notify.html' %}
{% include 'partials/trade-list.html' %}

163
core/views/accounts.py Normal file
View File

@ -0,0 +1,163 @@
import uuid
import orjson
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
from django.views import View
from rest_framework.parsers import FormParser, JSONParser
from rest_framework.views import APIView
from serde import ValidationError
from core.forms import AccountForm
from core.lib.serde import drakdoo
from core.models import Callback, Account
from core.util import logs
log = logs.get_logger(__name__)
def get_accounts(user):
accounts = Account.objects.filter(user=user)
return accounts
class Accounts(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/accounts.html"
async def get(self, request, type):
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
accounts = get_accounts(request.user)
if type == "page":
type = "modal"
context = {
"title": f"Accounts ({type})",
"unique": unique,
"window_content": self.window_content,
"items": accounts,
"type": type,
}
return render(request, template_name, context)
class AccountAction(LoginRequiredMixin, APIView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/add-account.html"
parser_classes = [FormParser]
def get(self, request, type, account_id=None):
"""
Get the form for adding or editing a account.
:param account_id: The id of the account to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if account_id:
try:
account = Account.objects.get(id=account_id, user=request.user)
form = AccountForm(instance=account)
except Account.DoesNotExist:
message = "Account does not exist"
message_class = "danger"
context = {
"message": message,
"message_class": message_class,
"window_content": self.window_content,
}
return render(request, template_name, context)
else:
form = AccountForm()
if type == "page":
type = "modal"
context = {
"form": form,
"account_id": account_id,
"type": type,
"unique": unique,
"window_content": self.window_content,
}
return render(request, template_name, context)
def put(self, request, type, account_id=None):
"""
Add or edit a account.
:param account_id: The id of the account to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
if account_id:
try:
form = AccountForm(request.data, instance=account.objects.get(id=account_id))
except account.DoesNotExist:
message = "Account does not exist"
message_class = "danger"
context = {
"message": message,
"class": message_class,
}
return render(request, self.template_name, context)
else:
form = AccountForm(request.data)
if form.is_valid():
account = form.save(commit=False)
account.user = request.user
account.save()
if account_id:
message = f"Account {account_id} edited successfully"
else:
message = f"Account {account.id} added successfully"
else:
message = "Error adding account"
message_class = "danger"
accounts = get_accounts(request.user)
context = {
"items": accounts,
"type": type,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/account-list.html"
return render(request, template_name, context)
def delete(self, request, type, account_id):
"""
Delete a account.
:param account_id: The id of the account to delete.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
try:
account = Account.objects.get(id=account_id, user=request.user)
account.delete()
message = "Account deleted successfully"
except Account.DoesNotExist:
message = "Error deleting account"
message_class = "danger"
accounts = get_accounts(request.user)
context = {
"items": accounts,
"type": type,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/account-list.html"
return render(request, template_name, context)

View File

@ -8,11 +8,11 @@ from django.views import View
from core.models import Callback, Hook from core.models import Callback, Hook
def get_callbacks(hook=None, user=None): def get_callbacks(user, hook=None):
if user: if hook:
callbacks = Callback.objects.filter(hook=hook, hook__user=user)
else:
callbacks = Callback.objects.filter(hook__user=user) callbacks = Callback.objects.filter(hook__user=user)
elif hook:
callbacks = Callback.objects.filter(hook=hook)
return callbacks return callbacks
@ -38,9 +38,9 @@ class Callbacks(LoginRequiredMixin, View):
"type": type, "type": type,
} }
return render(request, template_name, context) return render(request, template_name, context)
callbacks = get_callbacks(hook) callbacks = get_callbacks(request.user, hook)
else: else:
callbacks = get_callbacks(user=request.user) callbacks = get_callbacks(request.user)
if type == "page": if type == "page":
type = "modal" type = "modal"

179
core/views/trades.py Normal file
View File

@ -0,0 +1,179 @@
import uuid
import orjson
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
from django.views import View
from rest_framework.parsers import FormParser, JSONParser
from rest_framework.views import APIView
from serde import ValidationError
from core.forms import TradeForm
from core.lib.serde import drakdoo
from core.models import Callback, Trade, Account
from core.util import logs
log = logs.get_logger(__name__)
def get_trades(user, account=None):
if user:
trades = Trade.objects.filter(account__user=user)
elif account:
trades = Trade.objects.filter(account=account, account__user=user)
return trades
class Trades(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/trades.html"
async def get(self, request, type, account_id=None):
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if account_id:
try:
trades = Account.objects.get(id=account_id, user=request.user)
except Account.DoesNotExist:
message = "Account does not exist."
message_class = "danger"
context = {
"message": message,
"class": message_class,
"type": type,
}
return render(request, template_name, context)
trades = get_trades(request.user, account_id)
else:
trades = get_trades(request.user)
if type == "page":
type = "modal"
context = {
"title": f"Trades ({type})",
"unique": unique,
"window_content": self.window_content,
"items": trades,
"type": type,
}
return render(request, template_name, context)
class TradeAction(LoginRequiredMixin, APIView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/add-trade.html"
parser_classes = [FormParser]
def get(self, request, type, trade_id=None):
"""
Get the form for adding or editing a trade.
:param trade_id: The id of the trade to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if trade_id:
try:
trade = Trade.objects.get(id=trade_id, account__user=request.user)
form = TradeForm(instance=trade)
except Trade.DoesNotExist:
message = "Trade does not exist"
message_class = "danger"
context = {
"message": message,
"message_class": message_class,
"window_content": self.window_content,
}
return render(request, template_name, context)
else:
form = TradeForm()
if type == "page":
type = "modal"
context = {
"form": form,
"trade_id": trade_id,
"type": type,
"unique": unique,
"window_content": self.window_content,
}
return render(request, template_name, context)
def put(self, request, type, trade_id=None):
"""
Add or edit a trade.
:param trade_id: The id of the trade to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
if trade_id:
try:
form = TradeForm(request.data, instance=Trade.objects.get(id=trade_id))
except Trade.DoesNotExist:
message = "Trade does not exist"
message_class = "danger"
context = {
"message": message,
"class": message_class,
}
return render(request, self.template_name, context)
else:
form = TradeForm(request.data)
if form.is_valid():
trade = form.save(commit=False)
trade.save()
if trade_id:
message = f"Trade {trade_id} edited successfully"
else:
message = f"Trade {trade.id} added successfully"
else:
message = "Error adding trade"
message_class = "danger"
trades = get_trades(request.user)
context = {
"items": trades,
"type": type,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/trade-list.html"
return render(request, template_name, context)
def delete(self, request, type, trade_id):
"""
Delete a trade.
:param trade_id: The id of the trade to delete.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
try:
trade = Trade.objects.get(id=trade_id, account__user=request.user)
trade.delete()
message = "trade deleted successfully"
except Trade.DoesNotExist:
message = "Error deleting trade"
message_class = "danger"
trades = get_trades(request.user)
context = {
"items": trades,
"type": type,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/trade-list.html"
return render(request, template_name, context)

View File

@ -16,3 +16,4 @@ orjson
django-otp django-otp
qrcode qrcode
serde[ext] serde[ext]
ccxt