Use ObjectRead helper for all list and detail views

This commit is contained in:
Mark Veidemanis 2022-12-08 07:20:46 +00:00
parent 1e85e830b2
commit 8840b04059
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
12 changed files with 321 additions and 354 deletions

View File

@ -0,0 +1,72 @@
{% load pretty %}
{% include 'partials/notify.html' %}
{% if live is not None %}
<h1 class="title">Live {{ context_object_name_singular }} info</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% block live_tbody %}
{% for key, item in live.items %}
{% if key in pretty %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
<pre>{{ item|pretty }}</pre>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% endblock %}
</tbody>
</table>
{% endif %}
{% if object is not None %}
<h1 class="title">{{ title_singular }} info</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% block tbody %}
{% for key, item in object.items %}
{% if key in pretty %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
<pre>{{ item|pretty }}</pre>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
{% endblock %}
</tbody>
</table>
{% endif %}

View File

@ -0,0 +1,33 @@
{% extends 'partials/generic-detail.html' %}
{% block tbody %}
{% for key, item in object.items %}
<tr>
{% if key == 'trade_ids' %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{% for trade_id in item %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type trade_id=trade_id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
{{ trade_id }}
</button>
{% endfor %}
{% endif %}
</td>
{% else %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
{% endblock %}

View File

@ -1,53 +0,0 @@
{% load pretty %}
{% include 'partials/notify.html' %}
<h1 class="title">Live information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in live_info.items %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1 class="title">Stored information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in db_info.items %}
{% if key == 'instruments' %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
<pre>{{ item|pretty }}</pre>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@ -0,0 +1,45 @@
{% include 'partials/notify.html' %}
{% if page_title is not None %}
<h1 class="title is-4">{{ page_title }}</h1>
{% endif %}
{% if page_subtitle is not None %}
<h1 class="subtitle">{{ page_subtitle }}</h1>
{% endif %}
<div class="buttons">
{% if submit_url is not None %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ submit_url }}"
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>{{ title_singular }}</span>
</span>
</button>
{% endif %}
{% if delete_all_url is not None %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{{ delete_all_url }}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete all {{ context_object_name }}?"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
<span>Delete all {{ context_object_name }} </span>
</span>
</button>
{% endif %}
</div>
{% include detail_template %}

View File

@ -1,53 +0,0 @@
{% load pretty %}
{% include 'partials/notify.html' %}
<h1 class="title">Live information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in live_info.items %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1 class="title">Stored information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in db_info.items %}
{% if key == 'response' %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
<pre>{{ item|pretty }}</pre>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@ -1,39 +0,0 @@
{% include 'partials/notify.html' %}
<h1 class="title">Live information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in items.items %}
<tr>
{% if key == 'trade_ids' %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{% for trade_id in item %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type trade_id=trade_id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
{{ trade_id }}
</button>
{% endfor %}
{% endif %}
</td>
{% else %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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):
"""

View File

@ -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

View File

@ -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):