diff --git a/core/templates/partials/generic-detail.html b/core/templates/partials/generic-detail.html
new file mode 100644
index 0000000..3eee173
--- /dev/null
+++ b/core/templates/partials/generic-detail.html
@@ -0,0 +1,72 @@
+{% load pretty %}
+{% include 'partials/notify.html' %}
+
+{% if live is not None %}
+
Live {{ context_object_name_singular }} info
+
+
+ attribute
+ value
+
+
+ {% block live_tbody %}
+ {% for key, item in live.items %}
+ {% if key in pretty %}
+
+ {{ key }}
+
+ {% if item is not None %}
+ {{ item|pretty }}
+ {% endif %}
+
+
+ {% else %}
+
+ {{ key }}
+
+ {% if item is not None %}
+ {{ item }}
+ {% endif %}
+
+
+ {% endif %}
+ {% endfor %}
+ {% endblock %}
+
+
+{% endif %}
+
+{% if object is not None %}
+ {{ title_singular }} info
+
+
+ attribute
+ value
+
+
+ {% block tbody %}
+ {% for key, item in object.items %}
+ {% if key in pretty %}
+
+ {{ key }}
+
+ {% if item is not None %}
+ {{ item|pretty }}
+ {% endif %}
+
+
+ {% else %}
+
+ {{ key }}
+
+ {% if item is not None %}
+ {{ item }}
+ {% endif %}
+
+
+ {% endif %}
+ {% endfor %}
+ {% endblock %}
+
+
+{% endif %}
\ No newline at end of file
diff --git a/core/templates/partials/position-detail.html b/core/templates/partials/position-detail.html
new file mode 100644
index 0000000..9b06a5e
--- /dev/null
+++ b/core/templates/partials/position-detail.html
@@ -0,0 +1,33 @@
+{% extends 'partials/generic-detail.html' %}
+
+{% block tbody %}
+ {% for key, item in object.items %}
+
+ {% if key == 'trade_ids' %}
+ {{ key }}
+
+ {% if item is not None %}
+ {% for trade_id in item %}
+
+ {{ trade_id }}
+
+ {% endfor %}
+ {% endif %}
+
+ {% else %}
+ {{ key }}
+
+ {% if item is not None %}
+ {{ item }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/core/templates/window-content/account-info.html b/core/templates/window-content/account-info.html
deleted file mode 100644
index 729484e..0000000
--- a/core/templates/window-content/account-info.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% load pretty %}
-{% include 'partials/notify.html' %}
-
-Live information
-
-
- attribute
- value
-
-
- {% for key, item in live_info.items %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item }}
- {% endif %}
-
-
- {% endfor %}
-
-
-
-Stored information
-
-
- attribute
- value
-
-
- {% for key, item in db_info.items %}
- {% if key == 'instruments' %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item|pretty }}
- {% endif %}
-
-
- {% else %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item }}
- {% endif %}
-
-
- {% endif %}
- {% endfor %}
-
-
\ No newline at end of file
diff --git a/core/templates/window-content/object.html b/core/templates/window-content/object.html
new file mode 100644
index 0000000..d772d72
--- /dev/null
+++ b/core/templates/window-content/object.html
@@ -0,0 +1,45 @@
+{% include 'partials/notify.html' %}
+{% if page_title is not None %}
+ {{ page_title }}
+{% endif %}
+{% if page_subtitle is not None %}
+ {{ page_subtitle }}
+{% endif %}
+
+
+ {% if submit_url is not None %}
+
+
+
+
+
+ {{ title_singular }}
+
+
+ {% endif %}
+ {% if delete_all_url is not None %}
+
+
+
+
+
+ Delete all {{ context_object_name }}
+
+
+ {% endif %}
+
+
+{% include detail_template %}
+
diff --git a/core/templates/window-content/trade.html b/core/templates/window-content/trade.html
deleted file mode 100644
index cf2c4ce..0000000
--- a/core/templates/window-content/trade.html
+++ /dev/null
@@ -1,53 +0,0 @@
-{% load pretty %}
-{% include 'partials/notify.html' %}
-
-Live information
-
-
- attribute
- value
-
-
- {% for key, item in live_info.items %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item }}
- {% endif %}
-
-
- {% endfor %}
-
-
-
-Stored information
-
-
- attribute
- value
-
-
- {% for key, item in db_info.items %}
- {% if key == 'response' %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item|pretty }}
- {% endif %}
-
-
- {% else %}
-
- {{ key }}
-
- {% if item is not None %}
- {{ item }}
- {% endif %}
-
-
- {% endif %}
- {% endfor %}
-
-
\ No newline at end of file
diff --git a/core/templates/window-content/view-position.html b/core/templates/window-content/view-position.html
deleted file mode 100644
index 9f32cc3..0000000
--- a/core/templates/window-content/view-position.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{% include 'partials/notify.html' %}
-Live information
-
-
- attribute
- value
-
-
- {% for key, item in items.items %}
-
- {% if key == 'trade_ids' %}
- {{ key }}
-
- {% if item is not None %}
- {% for trade_id in item %}
-
- {{ trade_id }}
-
- {% endfor %}
- {% endif %}
-
- {% else %}
- {{ key }}
-
- {% if item is not None %}
- {{ item }}
- {% endif %}
-
- {% endif %}
-
- {% endfor %}
-
-
diff --git a/core/views/__init__.py b/core/views/__init__.py
index f84ec5f..d7fbef2 100644
--- a/core/views/__init__.py
+++ b/core/views/__init__.py
@@ -3,7 +3,7 @@ import uuid
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Paginator
from django.db.models import QuerySet
-from django.http import Http404, HttpResponseBadRequest
+from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.urls import reverse
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
@@ -105,15 +105,20 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
# copied from BaseListView
def get(self, request, *args, **kwargs):
- self.request = request
- self.object_list = self.get_queryset(**kwargs)
- allow_empty = self.get_allow_empty()
-
type = kwargs.get("type", None)
if not type:
return HttpResponseBadRequest("No type specified")
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
+
+ self.request = request
+ self.object_list = self.get_queryset(**kwargs)
+ if isinstance(self.object_list, HttpResponse):
+ return self.object_list
+ if isinstance(self.object_list, HttpResponseBadRequest):
+ return self.object_list
+ allow_empty = self.get_allow_empty()
+
self.template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
@@ -208,8 +213,6 @@ class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView):
return self.get(self.request, **self.kwargs, form=form)
def get(self, request, *args, **kwargs):
- self.request = request
- self.kwargs = kwargs
type = kwargs.get("type", None)
if not type:
return HttpResponseBadRequest("No type specified")
@@ -218,6 +221,9 @@ class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView):
self.template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
+ self.request = request
+ self.kwargs = kwargs
+
list_url_args = {}
for arg in self.list_url_args:
list_url_args[arg] = locals()[arg]
@@ -253,8 +259,75 @@ class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView):
class ObjectRead(RestrictedViewMixin, ObjectNameMixin, DetailView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/object.html"
+ detail_template = "partials/generic-detail.html"
+
+ page_title = None
+ page_subtitle = None
model = None
+ # submit_url_name = None
+
+ detail_url_name = None
+ # WARNING: TAKEN FROM locals()
+ detail_url_args = ["type"]
+
+ request = None
+
+ def get(self, request, *args, **kwargs):
+ type = kwargs.get("type", None)
+ if not type:
+ return HttpResponseBadRequest("No type specified")
+ if type not in self.allowed_types:
+ return HttpResponseBadRequest()
+ self.template_name = f"wm/{type}.html"
+ unique = str(uuid.uuid4())[:8]
+
+ detail_url_args = {}
+ for arg in self.detail_url_args:
+ if arg in locals():
+ detail_url_args[arg] = locals()[arg]
+ elif arg in kwargs:
+ detail_url_args[arg] = kwargs[arg]
+
+ self.request = request
+ self.object = self.get_object(**kwargs)
+ if isinstance(self.object, HttpResponse):
+ return self.object
+
+ orig_type = type
+ if type == "page":
+ type = "modal"
+
+ context = self.get_context_data()
+
+ context["title"] = self.title + f" ({type})"
+ context["title_singular"] = self.title_singular
+ context["unique"] = unique
+ context["window_content"] = self.window_content
+ context["detail_template"] = self.detail_template
+ if self.page_title:
+ context["page_title"] = self.page_title
+ if self.page_subtitle:
+ context["page_subtitle"] = self.page_subtitle
+ context["type"] = type
+ context["context_object_name"] = self.context_object_name
+ context["context_object_name_singular"] = self.context_object_name_singular
+
+ if self.detail_url_name is not None:
+ context["detail_url"] = reverse(
+ self.detail_url_name, kwargs=detail_url_args
+ )
+
+ # Return partials for HTMX
+ if self.request.htmx:
+ if request.headers["HX-Target"] == self.context_object_name + "-info":
+ self.template_name = self.detail_template
+ elif orig_type == "page":
+ self.template_name = self.detail_template
+ else:
+ context["window_content"] = self.detail_template
+
+ return self.render_to_response(context)
class ObjectUpdate(RestrictedViewMixin, ObjectNameMixin, UpdateView):
diff --git a/core/views/accounts.py b/core/views/accounts.py
index 2c6d0a3..8c543cc 100644
--- a/core/views/accounts.py
+++ b/core/views/accounts.py
@@ -1,20 +1,21 @@
-import uuid
-
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponseBadRequest
-from django.shortcuts import render
-from django.views import View
from two_factor.views.mixins import OTPRequiredMixin
from core.forms import AccountForm
from core.models import Account
from core.util import logs
-from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
+from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
log = logs.get_logger(__name__)
-class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
+class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
+ context_object_name_singular = "account"
+ context_object_name = "accounts"
+
+ detail_url_name = "account_info"
+ detail_url_args = ["type", "pk"]
+
VIEWABLE_FIELDS_MODEL = [
"name",
"exchange",
@@ -24,20 +25,11 @@ class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
"supported_symbols",
# "instruments",
]
- allowed_types = ["modal", "widget", "window", "page"]
- window_content = "window-content/account-info.html"
- def get(self, request, type, pk):
- """
- Get the account details.
- :param account_id: The id of the account.
- """
- if type not in self.allowed_types:
- return HttpResponseBadRequest
- template_name = f"wm/{type}.html"
- unique = str(uuid.uuid4())[:8]
+ def get_object(self, **kwargs):
+ pk = kwargs.get("pk")
try:
- account = Account.get_by_id(pk, request.user)
+ account = Account.get_by_id(pk, self.request.user)
except Account.DoesNotExist:
message = "Account does not exist"
message_class = "danger"
@@ -46,8 +38,7 @@ class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
"message_class": message_class,
"window_content": self.window_content,
}
- return render(request, template_name, context)
-
+ return self.render_to_response(context)
live_info = account.client.get_account()
live_info = live_info
account_info = account.__dict__
@@ -56,18 +47,8 @@ class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
}
account_info["supported_symbols"] = ", ".join(account_info["supported_symbols"])
- if type == "page":
- type = "modal"
- context = {
- "db_info": account_info,
- "live_info": live_info,
- "pk": pk,
- "type": type,
- "unique": unique,
- "window_content": self.window_content,
- }
-
- return render(request, template_name, context)
+ self.extra_context = {"live": live_info}
+ return account_info
class AccountList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
@@ -91,19 +72,6 @@ class AccountCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
submit_url_name = "account_create"
-# class AccountRead(LoginRequiredMixin, ObjectRead):
-# model = Account
-# context_object_name = "accounts"
-# submit_url_name = "account_read"
-# fields = (
-# "name",
-# "exchange",
-# "api_key",
-# "api_secret",
-# "sandbox",
-# )
-
-
class AccountUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
model = Account
form_class = AccountForm
diff --git a/core/views/callbacks.py b/core/views/callbacks.py
index 8598761..85ff3eb 100644
--- a/core/views/callbacks.py
+++ b/core/views/callbacks.py
@@ -1,11 +1,8 @@
-import uuid
-
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest
-from django.shortcuts import render
-from django.views import View
from core.models import Callback, Hook, Signal
+from core.views import ObjectList
def get_callbacks(user, hook=None, signal=None):
@@ -17,21 +14,30 @@ def get_callbacks(user, hook=None, signal=None):
return callbacks
-class Callbacks(LoginRequiredMixin, View):
- allowed_types = ["modal", "widget", "window", "page"]
- window_content = "window-content/objects.html"
+class Callbacks(LoginRequiredMixin, ObjectList):
list_template = "partials/callback-list.html"
page_title = "List of received callbacks"
- async def get(self, request, type, object_type, object_id):
- if type not in self.allowed_types:
- return HttpResponseBadRequest
- template_name = f"wm/{type}.html"
- unique = str(uuid.uuid4())[:8]
+ context_object_name_singular = "callback"
+ context_object_name = "callbacks"
+ list_url_name = "callbacks"
+ list_url_args = ["type", "object_type", "object_id"]
+
+ def get_callbacks(self, user, hook=None, signal=None):
+ if hook:
+ cast = {"hook": hook, "hook__user": user}
+ elif signal:
+ cast = {"signal": signal, "signal__user": user}
+ callbacks = Callback.objects.filter(**cast)
+ return callbacks
+
+ def get_queryset(self, **kwargs):
+ object_type = kwargs.get("object_type")
+ object_id = kwargs.get("object_id")
if object_type == "hook":
try:
- hook = Hook.objects.get(id=object_id, user=request.user)
+ hook = Hook.objects.get(id=object_id, user=self.request.user)
except Hook.DoesNotExist:
message = "Hook does not exist."
message_class = "danger"
@@ -40,11 +46,11 @@ class Callbacks(LoginRequiredMixin, View):
"class": message_class,
"type": type,
}
- return render(request, template_name, context)
- callbacks = get_callbacks(request.user, hook)
+ return self.render_to_response(context)
+ callbacks = self.get_callbacks(self.request.user, hook)
elif object_type == "signal":
try:
- signal = Signal.objects.get(id=object_id, user=request.user)
+ signal = Signal.objects.get(id=object_id, user=self.request.user)
except Signal.DoesNotExist:
message = "Signal does not exist."
message_class = "danger"
@@ -53,20 +59,9 @@ class Callbacks(LoginRequiredMixin, View):
"class": message_class,
"type": type,
}
- return render(request, template_name, context)
- callbacks = get_callbacks(request.user, signal=signal)
+ return self.render_to_response(context)
+ callbacks = self.get_callbacks(self.request.user, signal=signal)
else:
- callbacks = get_callbacks(request.user)
- if type == "page":
- type = "modal"
+ return HttpResponseBadRequest("Invalid object type")
- context = {
- "title": f"Callbacks ({type})",
- "unique": unique,
- "window_content": self.window_content,
- "list_template": self.list_template,
- "object_list": callbacks,
- "type": type,
- "page_title": self.page_title,
- }
- return render(request, template_name, context)
+ return callbacks
diff --git a/core/views/positions.py b/core/views/positions.py
index e8ea332..c67c40a 100644
--- a/core/views/positions.py
+++ b/core/views/positions.py
@@ -1,16 +1,11 @@
-import uuid
-
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponseBadRequest
from django.shortcuts import render
-from django.views import View
-from rest_framework.parsers import FormParser
from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError
from core.models import Account, Trade
from core.util import logs
-from core.views import ObjectList
+from core.views import ObjectList, ObjectRead
log = logs.get_logger(__name__)
@@ -71,42 +66,27 @@ class Positions(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
return items
-class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View):
- allowed_types = ["modal", "widget", "window", "page"]
- window_content = "window-content/view-position.html"
- parser_classes = [FormParser]
+class PositionAction(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
+ detail_template = "partials/position-detail.html"
- def get(self, request, type, account_id, symbol):
- """
- Get live information for a trade.
- """
- if type not in self.allowed_types:
- return HttpResponseBadRequest()
- template_name = f"wm/{type}.html"
- unique = str(uuid.uuid4())[:8]
+ context_object_name_singular = "position"
+ context_object_name = "positions"
- account = Account.get_by_id(account_id, request.user)
+ detail_url_name = "position_action"
+ detail_url_args = ["type", "account_id", "symbol"]
+
+ def get_object(self, **kwargs):
+ account_id = kwargs.get("account_id")
+ symbol = kwargs.get("symbol")
+ account = Account.get_by_id(account_id, self.request.user)
info = account.client.get_position_info(symbol)
- valid_trade_ids = list(
- annotate_positions([info], request.user, return_order_ids=True)
- )
-
- # Remove some fields from the info dict
del info["long"]
del info["short"]
-
- if type == "page":
- type = "modal"
- context = {
- "title": f"Position info ({type})",
- "unique": unique,
- "window_content": self.window_content,
- "type": type,
- "items": info,
- "valid_trade_ids": valid_trade_ids,
- }
-
- return render(request, template_name, context)
+ valid_trade_ids = list(
+ annotate_positions([info], self.request.user, return_order_ids=True)
+ )
+ self.extra_context = {"valid_trade_ids": valid_trade_ids}
+ return info
def delete(self, request, account_id, side, symbol):
"""
diff --git a/core/views/profit.py b/core/views/profit.py
index d77e0ca..8b367e1 100644
--- a/core/views/profit.py
+++ b/core/views/profit.py
@@ -1,80 +1,39 @@
-import uuid
-
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponseBadRequest
-from django.shortcuts import render
-from django.urls import reverse
-from django.views import View
from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError
from core.models import Account
from core.util import logs
+from core.views import ObjectList
log = logs.get_logger(__name__)
-def get_profit(user):
- items = []
- accounts = Account.objects.filter(user=user)
- for account in accounts:
- try:
- details = account.client.get_account()
- pl = details["pl"]
- item = {
- "account": account,
- "pl": float(pl),
- "balance": details["balance"],
- "currency": details["currency"],
- }
- items.append(item)
- except GenericAPIError:
- continue
- return items
-
-
-class Profit(LoginRequiredMixin, OTPRequiredMixin, View):
- allowed_types = ["modal", "widget", "window", "page"]
- window_content = "window-content/objects.html"
+class Profit(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/profit-list.html"
page_title = "Profit by account"
page_subtitle = None
+
context_object_name_singular = "profit"
context_object_name = "profit"
- def get(self, request, type):
- if type not in self.allowed_types:
- return HttpResponseBadRequest
- self.template_name = f"wm/{type}.html"
- unique = str(uuid.uuid4())[:8]
- items = get_profit(request.user)
+ list_url_name = "profit"
+ list_url_args = ["type"]
- orig_type = type
- if type == "page":
- type = "modal"
- cast = {
- "type": orig_type,
- }
- list_url = reverse("profit", kwargs={**cast})
- context = {
- "title": f"Profit ({type})",
- "unique": unique,
- "window_content": self.window_content,
- "list_template": self.list_template,
- "object_list": items,
- "type": type,
- "page_title": self.page_title,
- "page_subtitle": self.page_subtitle,
- "list_url": list_url,
- "context_object_name_singular": self.context_object_name_singular,
- "context_object_name": self.context_object_name,
- }
- # Return partials for HTMX
- if self.request.htmx:
- if request.headers["HX-Target"] == self.context_object_name + "-table":
- self.template_name = self.list_template
- elif orig_type == "page":
- self.template_name = self.list_template
- else:
- context["window_content"] = self.list_template
- return render(request, self.template_name, context)
+ def get_queryset(self, **kwargs):
+ items = []
+ accounts = Account.objects.filter(user=self.request.user)
+ for account in accounts:
+ try:
+ details = account.client.get_account()
+ pl = details["pl"]
+ item = {
+ "account": account,
+ "pl": float(pl),
+ "balance": details["balance"],
+ "currency": details["currency"],
+ }
+ items.append(item)
+ except GenericAPIError:
+ continue
+ return items
diff --git a/core/views/trades.py b/core/views/trades.py
index e253956..5e4b398 100644
--- a/core/views/trades.py
+++ b/core/views/trades.py
@@ -1,5 +1,3 @@
-import uuid
-
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest
from django.shortcuts import render
@@ -15,25 +13,23 @@ from core.views import (
ObjectDelete,
ObjectList,
ObjectNameMixin,
+ ObjectRead,
ObjectUpdate,
)
log = logs.get_logger(__name__)
-class TradeAction(LoginRequiredMixin, OTPRequiredMixin, View):
- allowed_types = ["modal", "widget", "window", "page"]
- window_content = "window-content/trade.html"
+class TradeAction(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
+ context_object_name_singular = "position"
+ context_object_name = "positions"
- def get(self, request, type, trade_id):
- """
- Get live information for a trade.
- """
- if type not in self.allowed_types:
- return HttpResponseBadRequest()
- template_name = f"wm/{type}.html"
- unique = str(uuid.uuid4())[:8]
- db_info = Trade.get_by_id_or_order(trade_id, request.user)
+ detail_url_name = "trade_action"
+ detail_url_args = ["type", "trade_id"]
+
+ def get_object(self, **kwargs):
+ trade_id = kwargs.get("trade_id")
+ db_info = Trade.get_by_id_or_order(trade_id, self.request.user)
if db_info is None:
return HttpResponseBadRequest("Trade not found.")
if db_info.order_id is not None:
@@ -50,21 +46,12 @@ class TradeAction(LoginRequiredMixin, OTPRequiredMixin, View):
else:
live_info = {}
- if type == "page":
- type = "modal"
db_info = db_info.__dict__
del db_info["_state"]
del db_info["_original"]
- context = {
- "title": f"Trade info ({type})",
- "unique": unique,
- "window_content": self.window_content,
- "type": type,
- "db_info": db_info,
- "live_info": live_info,
- }
- return render(request, template_name, context)
+ self.extra_context = {"live": live_info, "pretty": ["response"]}
+ return db_info
class TradeList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):