Use django-crud-mixins for CRUD helpers

This commit is contained in:
Mark Veidemanis 2023-02-10 20:49:35 +00:00
parent 659b73e695
commit 119acdd734
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
42 changed files with 47 additions and 880 deletions

View File

@ -2,6 +2,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm 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 mixins.restrictions import RestrictedFormMixin
from .models import ( from .models import (
Account, Account,
@ -20,36 +21,6 @@ from .models import (
# flake8: noqa: E501 # flake8: noqa: E501
class RestrictedFormMixin:
"""
This mixin is used to restrict the queryset of a form to the current user.
The request object is passed from the view.
Fieldargs is used to pass additional arguments to the queryset filter.
"""
fieldargs = {}
def __init__(self, *args, **kwargs):
# self.fieldargs = {}
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
for field in self.fields:
# Check it's not something like a CharField which has no queryset
if not hasattr(self.fields[field], "queryset"):
continue
model = self.fields[field].queryset.model
# Check if the model has a user field
try:
model._meta.get_field("user")
# Add the user to the queryset filters
self.fields[field].queryset = model.objects.filter(
user=self.request.user, **self.fieldargs.get(field, {})
)
except FieldDoesNotExist:
pass
class NewUserForm(UserCreationForm): class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True) email = forms.EmailField(required=True)

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="callbacks-table"> <table class="table is-fullwidth is-hoverable" id="callbacks-table">
<thead> <thead>

View File

@ -1 +0,0 @@
<button class="modal-close is-large" aria-label="close"></button>

View File

@ -1,3 +0,0 @@
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
onclick='grid.removeWidget("widget-{{ unique }}");grid.compact();'></i>

View File

@ -1,3 +0,0 @@
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
data-script="on click remove the closest <nav/>"></i>

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{% endblock %} {% endblock %}

View File

@ -1,72 +0,0 @@
{% 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

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,7 +0,0 @@
<div id="notification">
{% if message is not None %}
<div class="notification is-{{ class }}" hx-ext="remove-me" remove-me="3s">
{{ message }}
</div>
{% endif %}
</div>

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,4 +1,4 @@
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
<table <table
class="table is-fullwidth is-hoverable" class="table is-fullwidth is-hoverable"

View File

@ -1,34 +0,0 @@
{% 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 %}
{% load crispy_forms_tags %}
{% load crispy_forms_bulma_field %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{{ submit_url }}"
hx-target="#modals-here"
hx-swap="innerHTML">
{% csrf_token %}
{{ form|crispy }}
{% if hide_cancel is not True %}
<button
type="button"
class="button is-light modal-close-button">
Cancel
</button>
{% endif %}
<button type="submit" class="button modal-close-button">Submit</button>
</form>

View File

@ -1,45 +0,0 @@
{% 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">
<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">
<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,45 +0,0 @@
{% 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">
<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">
<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 list_template %}

View File

@ -1,20 +0,0 @@
{% load static %}
<script src="{% static 'modal.js' %}"></script>
{% block scripts %}
{% endblock %}
{% block styles %}
{% endblock %}
<div id="modal" class="modal is-active is-clipped">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
{% block modal_content %}
{% include window_content %}
{% endblock %}
{% include 'partials/close-modal.html' %}
</div>
</div>
</div>

View File

@ -1,6 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% include window_content %}
{% endblock %}

View File

@ -1,17 +0,0 @@
<nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %}
{% include 'partials/close-window.html' %}
{% endblock %}
{% block heading %}
{% endblock %}
</p>
<article class="panel-block is-active">
<div class="control">
{% block panel_content %}
{% endblock %}
</div>
</article>
</nav>

View File

@ -1,37 +0,0 @@
<div id="widget">
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}{% if widget_options is None %}gs-w="6" gs-h="1" gs-y="20" gs-x="0"{% else %}{% autoescape off %}{{ widget_options }}{% endautoescape %}{% endif %}{% endblock %}>
<div class="grid-stack-item-content">
<nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
{% block close_button %}
{% include 'partials/close-widget.html' %}
{% endblock %}
<i
class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
onclick='grid.compact();'></i>
{% block heading %}
{{ title }}
{% endblock %}
</p>
<article class="panel-block is-active">
<div class="control">
{% block panel_content %}
{% include window_content %}
{% endblock %}
</div>
</article>
</nav>
</div>
</div>
</div>
<script>
{% block custom_script %}
{% endblock %}
var widget_event = new Event('load-widget');
document.dispatchEvent(widget_event);
</script>
{% block custom_end %}
{% endblock %}

View File

@ -1,10 +0,0 @@
<magnet-block attract-distance="10" align-to="outer|center" class="floating-window">
{% extends 'wm/panel.html' %}
{% block heading %}
{{ title }}
{% endblock %}
{% block panel_content %}
{% include window_content %}
{% endblock %}
</magnet-block>

View File

@ -1,507 +0,0 @@
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, HttpResponse, HttpResponseBadRequest
from django.urls import reverse
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
from rest_framework.parsers import FormParser
class AbortSave(Exception):
pass
class RestrictedViewMixin:
"""
This mixin overrides two helpers in order to pass the user object to the filters.
get_queryset alters the objects returned for list views.
get_form_kwargs passes the request object to the form class. Remaining permissions
checks are in forms.py
"""
allow_empty = True
queryset = None
model = None
paginate_by = None
paginate_orphans = 0
context_object_name = None
paginator_class = Paginator
page_kwarg = "page"
ordering = None
def set_extra_args(self, user):
"""
This function is overriden to filter the objects by the requesting user.
"""
self.extra_permission_args = {}
def get_queryset(self, **kwargs):
"""
This function is overriden to filter the objects by the requesting user.
"""
self.set_extra_args(self.request.user)
if self.queryset is not None:
queryset = self.queryset
if isinstance(queryset, QuerySet):
# queryset = queryset.all()
queryset = queryset.filter(
user=self.request.user, **self.extra_permission_args
)
elif self.model is not None:
queryset = self.model._default_manager.filter(
user=self.request.user, **self.extra_permission_args
)
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
)
if hasattr(self, "get_ordering"):
ordering = self.get_ordering()
if ordering:
if isinstance(ordering, str):
ordering = (ordering,)
queryset = queryset.order_by(*ordering)
return queryset
def get_form_kwargs(self):
"""Passes the request object to the form class.
This is necessary to only display members that belong to a given user"""
kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
return kwargs
class ObjectNameMixin(object):
def __init__(self, *args, **kwargs):
if self.model is None:
self.title = self.context_object_name.title()
self.title_singular = self.context_object_name_singular.title()
else:
self.title_singular = self.model._meta.verbose_name.title() # Hook
self.context_object_name_singular = self.title_singular.lower() # hook
self.title = self.model._meta.verbose_name_plural.title() # Hooks
self.context_object_name = self.title.lower() # hooks
self.context_object_name = self.context_object_name.replace(" ", "")
self.context_object_name_singular = (
self.context_object_name_singular.replace(" ", "")
)
super().__init__(*args, **kwargs)
class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/objects.html"
list_template = None
page_title = None
page_subtitle = None
list_url_name = None
# WARNING: TAKEN FROM locals()
list_url_args = ["type"]
submit_url_name = None
submit_url_args = ["type"]
delete_all_url_name = None
widget_options = None
# copied from BaseListView
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("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]
list_url_args = {}
for arg in self.list_url_args:
if arg in locals():
list_url_args[arg] = locals()[arg]
elif arg in kwargs:
list_url_args[arg] = kwargs[arg]
orig_type = type
if type == "page":
type = "modal"
if not allow_empty:
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(
self.object_list, "exists"
):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
if is_empty:
raise Http404("Empty list")
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]
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["list_template"] = self.list_template
context["page_title"] = self.page_title
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.submit_url_name is not None:
context["submit_url"] = reverse(
self.submit_url_name, kwargs=submit_url_args
)
if self.list_url_name is not None:
context["list_url"] = reverse(self.list_url_name, kwargs=list_url_args)
if self.delete_all_url_name:
context["delete_all_url"] = reverse(self.delete_all_url_name)
if self.widget_options:
context["widget_options"] = self.widget_options
# 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 self.render_to_response(context)
class ObjectCreate(RestrictedViewMixin, ObjectNameMixin, CreateView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/object-form.html"
parser_classes = [FormParser]
page_title = None
page_subtitle = None
model = None
submit_url_name = None
submit_url_args = ["type"]
request = None
# Whether to hide the cancel button in the form
hide_cancel = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = "Create " + self.context_object_name_singular
def post_save(self, obj):
pass
def pre_save_mutate(self, user, obj):
pass
def form_valid(self, form):
obj = form.save(commit=False)
if self.request is None:
raise Exception("Request is None")
obj.user = self.request.user
try:
self.pre_save_mutate(self.request.user, obj)
except AbortSave as e:
context = {"message": f"Failed to save: {e}", "class": "danger"}
return self.render_to_response(context)
obj.save()
form.save_m2m()
self.post_save(obj)
context = {"message": "Object created", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
return self.get(self.request, **self.kwargs, form=form)
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("Invalid type specified")
self.template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
self.request = request
self.kwargs = kwargs
if type == "widget":
self.hide_cancel = True
if type == "page":
type = "modal"
self.object = None
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()
form = kwargs.get("form", None)
if form:
context["form"] = form
context["unique"] = unique
context["window_content"] = self.window_content
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
context["submit_url"] = submit_url
context["type"] = type
context["hide_cancel"] = self.hide_cancel
if self.page_title:
context["page_title"] = self.page_title
if self.page_subtitle:
context["page_subtitle"] = self.page_subtitle
response = self.render_to_response(context)
# response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def post(self, request, *args, **kwargs):
self.request = request
self.template_name = "partials/notify.html"
return super().post(request, *args, **kwargs)
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):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/object-form.html"
parser_classes = [FormParser]
page_title = None
page_subtitle = None
model = None
submit_url_name = None
submit_url_args = ["type", "pk"]
request = None
# Whether pk is required in the get request
pk_required = True
# Whether to hide the cancel button in the form
hide_cancel = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = "Update " + self.context_object_name_singular
def post_save(self, obj):
pass
def form_valid(self, form):
obj = form.save(commit=False)
if self.request is None:
raise Exception("Request is None")
obj.save()
form.save_m2m()
self.post_save(obj)
context = {"message": "Object updated", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
return self.get(self.request, **self.kwargs, form=form)
def get(self, request, *args, **kwargs):
self.request = request
type = kwargs.get("type", None)
pk = kwargs.get("pk", None)
if not type:
return HttpResponseBadRequest("No type specified")
if not pk:
if self.pk_required:
return HttpResponseBadRequest("No pk specified")
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
self.template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if type == "widget":
self.hide_cancel = True
if type == "page":
type = "modal"
self.object = self.get_object()
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()
form = kwargs.get("form", None)
if form:
context["form"] = form
context["title"] = self.title + f" ({type})"
context["title_singular"] = self.title_singular
context["unique"] = unique
context["window_content"] = self.window_content
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
context["submit_url"] = submit_url
context["type"] = type
context["hide_cancel"] = self.hide_cancel
if self.page_title:
context["page_title"] = self.page_title
if self.page_subtitle:
context["page_subtitle"] = self.page_subtitle
response = self.render_to_response(context)
# response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def post(self, request, *args, **kwargs):
self.request = request
self.template_name = "partials/notify.html"
return super().post(request, *args, **kwargs)
class ObjectDelete(RestrictedViewMixin, ObjectNameMixin, DeleteView):
model = None
template_name = "partials/notify.html"
# Overriden to prevent success URL from being used
def delete(self, request, *args, **kwargs):
"""
Call the delete() method on the fetched object and then redirect to the
success URL.
"""
self.object = self.get_object()
# success_url = self.get_success_url()
self.object.delete()
context = {"message": "Object deleted", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
# This will be used in newer Django versions, until then we get a warning
def form_valid(self, form):
"""
Call the delete() method on the fetched object.
"""
self.object = self.get_object()
self.object.delete()
context = {"message": "Object deleted", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response

View File

@ -1,13 +1,16 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import (
ObjectCreate,
ObjectDelete,
ObjectList,
ObjectRead,
ObjectUpdate,
)
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.forms import AccountForm from core.forms import AccountForm
from core.models import Account from core.models import Account
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
# from mixins.views import ObjectCreate, ObjectDelete,
# #ObjectList, ObjectRead, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,9 +1,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import AbortSave, ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from core.forms import AssetGroupForm, AssetRestrictionForm from core.forms import AssetGroupForm, AssetRestrictionForm
from core.models import AssetGroup, AssetRestriction from core.models import AssetGroup, AssetRestriction
from core.util import logs from core.util import logs
from core.views import AbortSave, ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,8 +1,8 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from mixins.views import ObjectList
from core.models import Callback, Hook, Signal from core.models import Callback, Hook, Signal
from core.views import ObjectList
def get_callbacks(user, hook=None, signal=None): def get_callbacks(user, hook=None, signal=None):

View File

@ -4,6 +4,7 @@ from string import digits
import orjson import orjson
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from pydantic import ValidationError from pydantic import ValidationError
from rest_framework.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
@ -13,7 +14,6 @@ from core.lib.schemas.drakdoo_s import DrakdooCallback
from core.models import Callback, Hook, Signal from core.models import Callback, Hook, Signal
from core.trading import market from core.trading import market
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,11 +1,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from mixins.views import (
from core.forms import TradingTimeForm
from core.models import Strategy, TradingTime
from core.util import logs
from core.views import (
ObjectCreate, ObjectCreate,
ObjectDelete, ObjectDelete,
ObjectList, ObjectList,
@ -13,6 +9,10 @@ from core.views import (
ObjectUpdate, ObjectUpdate,
) )
from core.forms import TradingTimeForm
from core.models import Strategy, TradingTime
from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -40,7 +40,7 @@ class TrendDirectionList(LoginRequiredMixin, ObjectList):
class TrendDirectionFlip(LoginRequiredMixin, ObjectNameMixin, View): class TrendDirectionFlip(LoginRequiredMixin, ObjectNameMixin, View):
template_name = "partials/notify.html" template_name = "mixins/partials/notify.html"
model = Strategy model = Strategy
def get(self, request, strategy_id, symbol): def get(self, request, strategy_id, symbol):
@ -62,7 +62,7 @@ class TrendDirectionFlip(LoginRequiredMixin, ObjectNameMixin, View):
class TrendDirectionDelete(LoginRequiredMixin, ObjectNameMixin, View): class TrendDirectionDelete(LoginRequiredMixin, ObjectNameMixin, View):
template_name = "partials/notify.html" template_name = "mixins/partials/notify.html"
model = Strategy model = Strategy
def delete(self, request, strategy_id, symbol): def delete(self, request, strategy_id, symbol):

View File

@ -1,8 +1,8 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectUpdate
from core.forms import NotificationSettingsForm from core.forms import NotificationSettingsForm
from core.models import NotificationSettings from core.models import NotificationSettings
from core.views import ObjectUpdate
# Notifications - we create a new notification settings object if there isn't one # Notifications - we create a new notification settings object if there isn't one

View File

@ -1,11 +1,11 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render from django.shortcuts import render
from mixins.views import ObjectList, ObjectRead
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError from core.exchanges import GenericAPIError
from core.models import Account, Trade from core.models import Account, Trade
from core.util import logs from core.util import logs
from core.views import ObjectList, ObjectRead
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -93,7 +93,7 @@ class PositionAction(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
""" """
Close a position. Close a position.
""" """
template_name = "partials/notify.html" template_name = "mixins/partials/notify.html"
account = Account.get_by_id(account_id, request.user) account = Account.get_by_id(account_id, request.user)
try: try:
api_response = account.client.close_position(side, symbol) api_response = account.client.close_position(side, symbol)

View File

@ -1,12 +1,12 @@
from decimal import Decimal as D from decimal import Decimal as D
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectList
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError from core.exchanges import GenericAPIError
from core.models import Account from core.models import Account
from core.util import logs from core.util import logs
from core.views import ObjectList
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,9 +1,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from core.forms import RiskModelForm from core.forms import RiskModelForm
from core.models import RiskModel from core.models import RiskModel
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,9 +1,9 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
from core.forms import SignalForm from core.forms import SignalForm
from core.models import Signal from core.models import Signal
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -1,4 +1,5 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from mixins.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
# from django.urls import reverse # from django.urls import reverse
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
@ -6,7 +7,6 @@ from two_factor.views.mixins import OTPRequiredMixin
from core.forms import StrategyForm from core.forms import StrategyForm
from core.models import Strategy from core.models import Strategy
from core.util import logs from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__) log = logs.get_logger(__name__)

View File

@ -2,13 +2,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from two_factor.views.mixins import OTPRequiredMixin from mixins.views import (
from core.exchanges import GenericAPIError
from core.forms import TradeForm
from core.models import Trade
from core.util import logs
from core.views import (
ObjectCreate, ObjectCreate,
ObjectDelete, ObjectDelete,
ObjectList, ObjectList,
@ -16,6 +10,12 @@ from core.views import (
ObjectRead, ObjectRead,
ObjectUpdate, ObjectUpdate,
) )
from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError
from core.forms import TradeForm
from core.models import Trade
from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -93,7 +93,7 @@ class TradeDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
class TradeDeleteAll(LoginRequiredMixin, OTPRequiredMixin, ObjectNameMixin, View): class TradeDeleteAll(LoginRequiredMixin, OTPRequiredMixin, ObjectNameMixin, View):
template_name = "partials/notify.html" template_name = "mixins/partials/notify.html"
model = Trade model = Trade
def delete(self, request): def delete(self, request):