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

+ + + + + + + {% block live_tbody %} + {% for key, item in live.items %} + {% if key in pretty %} + + + + + {% else %} + + + + + {% endif %} + {% endfor %} + {% endblock %} + +
attributevalue
{{ key }} + {% if item is not None %} +
{{ item|pretty }}
+ {% endif %} +
{{ key }} + {% if item is not None %} + {{ item }} + {% endif %} +
+{% endif %} + +{% if object is not None %} +

{{ title_singular }} info

+ + + + + + + {% block tbody %} + {% for key, item in object.items %} + {% if key in pretty %} + + + + + {% else %} + + + + + {% endif %} + {% endfor %} + {% endblock %} + +
attributevalue
{{ key }} + {% if item is not None %} +
{{ item|pretty }}
+ {% endif %} +
{{ key }} + {% if item is not None %} + {{ item }} + {% endif %} +
+{% 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 %} + + {% 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

- - - - - - - {% for key, item in live_info.items %} - - - - - {% endfor %} - -
attributevalue
{{ key }} - {% if item is not None %} - {{ item }} - {% endif %} -
- -

Stored information

- - - - - - - {% for key, item in db_info.items %} - {% if key == 'instruments' %} - - - - - {% else %} - - - - - {% endif %} - {% endfor %} - -
attributevalue
{{ key }} - {% if item is not None %} -
{{ item|pretty }}
- {% endif %} -
{{ key }} - {% if item is not None %} - {{ item }} - {% endif %} -
\ 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 %} + + {% endif %} + {% if delete_all_url is not None %} + + {% 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

- - - - - - - {% for key, item in live_info.items %} - - - - - {% endfor %} - -
attributevalue
{{ key }} - {% if item is not None %} - {{ item }} - {% endif %} -
- -

Stored information

- - - - - - - {% for key, item in db_info.items %} - {% if key == 'response' %} - - - - - {% else %} - - - - - {% endif %} - {% endfor %} - -
attributevalue
{{ key }} - {% if item is not None %} -
{{ item|pretty }}
- {% endif %} -
{{ key }} - {% if item is not None %} - {{ item }} - {% endif %} -
\ 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

- - - - - - - {% for key, item in items.items %} - - {% if key == 'trade_ids' %} - - - {% else %} - - - {% endif %} - - {% endfor %} - -
attributevalue
{{ key }} - {% if item is not None %} - {% for trade_id in item %} - - {% endfor %} - {% endif %} - {{ key }} - {% if item is not None %} - {{ item }} - {% endif %} -
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):