[ADD] BASE: Horilla generic bulk update method
This commit is contained in:
@@ -4,13 +4,15 @@ horilla/cbv_methods.py
|
||||
|
||||
import types
|
||||
import uuid
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
from venv import logger
|
||||
|
||||
from django import template
|
||||
from django import forms, template
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache as CACHE
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import ForeignKey
|
||||
from django.db.models.fields.related_descriptors import (
|
||||
ForwardManyToOneDescriptor,
|
||||
@@ -31,6 +33,67 @@ from horilla import settings
|
||||
from horilla.horilla_middlewares import _thread_locals
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
FIELD_WIDGET_MAP = {
|
||||
models.CharField: forms.TextInput(attrs={"class": "oh-input w-100"}),
|
||||
models.ImageField: forms.FileInput(
|
||||
attrs={"type": "file", "class": "oh-input w-100"}
|
||||
),
|
||||
models.FileField: forms.FileInput(
|
||||
attrs={"type": "file", "class": "oh-input w-100"}
|
||||
),
|
||||
models.TextField: forms.Textarea(
|
||||
{
|
||||
"class": "oh-input w-100",
|
||||
"rows": 2,
|
||||
"cols": 40,
|
||||
}
|
||||
),
|
||||
models.IntegerField: forms.NumberInput(attrs={"class": "oh-input w-100"}),
|
||||
models.FloatField: forms.NumberInput(attrs={"class": "oh-input w-100"}),
|
||||
models.DecimalField: forms.NumberInput(attrs={"class": "oh-input w-100"}),
|
||||
models.EmailField: forms.EmailInput(attrs={"class": "oh-input w-100"}),
|
||||
models.DateField: forms.DateInput(
|
||||
attrs={"type": "date", "class": "oh-input w-100"}
|
||||
),
|
||||
models.DateTimeField: forms.DateTimeInput(
|
||||
attrs={"type": "date", "class": "oh-input w-100"}
|
||||
),
|
||||
models.TimeField: forms.TimeInput(
|
||||
attrs={"type": "time", "class": "oh-input w-100"}
|
||||
),
|
||||
models.BooleanField: forms.Select({"class": "oh-select oh-select-2 w-100"}),
|
||||
models.ForeignKey: forms.Select({"class": "oh-select oh-select-2 w-100"}),
|
||||
models.ManyToManyField: forms.SelectMultiple(
|
||||
attrs={"class": "oh-select oh-select-2 select2-hidden-accessible"}
|
||||
),
|
||||
models.OneToOneField: forms.Select({"class": "oh-select oh-select-2 w-100"}),
|
||||
}
|
||||
|
||||
MODEL_FORM_FIELD_MAP = {
|
||||
models.CharField: forms.CharField,
|
||||
models.TextField: forms.CharField, # Textarea can be specified as a widget
|
||||
models.IntegerField: forms.IntegerField,
|
||||
models.FloatField: forms.FloatField,
|
||||
models.DecimalField: forms.DecimalField,
|
||||
models.ImageField: forms.FileField,
|
||||
models.FileField: forms.FileField,
|
||||
models.EmailField: forms.EmailField,
|
||||
models.DateField: forms.DateField,
|
||||
models.DateTimeField: forms.DateTimeField,
|
||||
models.TimeField: forms.TimeField,
|
||||
models.BooleanField: forms.BooleanField,
|
||||
models.ForeignKey: forms.ModelChoiceField,
|
||||
models.ManyToManyField: forms.ModelMultipleChoiceField,
|
||||
models.OneToOneField: forms.ModelChoiceField,
|
||||
}
|
||||
|
||||
|
||||
BOOLEAN_CHOICES = (
|
||||
("", "----------"),
|
||||
(True, "Yes"),
|
||||
(False, "No"),
|
||||
)
|
||||
|
||||
|
||||
def decorator_with_arguments(decorator):
|
||||
"""
|
||||
@@ -76,6 +139,10 @@ def decorator_with_arguments(decorator):
|
||||
|
||||
|
||||
def login_required(view_func):
|
||||
"""
|
||||
Decorator to check authenticity of users
|
||||
"""
|
||||
|
||||
def wrapped_view(self, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request")
|
||||
if not getattr(self, "request", None):
|
||||
@@ -108,6 +175,10 @@ def login_required(view_func):
|
||||
|
||||
@decorator_with_arguments
|
||||
def permission_required(function, perm):
|
||||
"""
|
||||
Decorator to validate user permissions
|
||||
"""
|
||||
|
||||
def _function(self, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request")
|
||||
if not getattr(self, "request", None):
|
||||
@@ -127,6 +198,20 @@ def permission_required(function, perm):
|
||||
return _function
|
||||
|
||||
|
||||
def hx_request_required(function):
|
||||
"""
|
||||
Decorator method that only allow HTMX metod to enter
|
||||
"""
|
||||
|
||||
def _function(request, *args, **kwargs):
|
||||
key = "HTTP_HX_REQUEST"
|
||||
if key not in request.META.keys():
|
||||
return render(request, "405.html")
|
||||
return function(request, *args, **kwargs)
|
||||
|
||||
return _function
|
||||
|
||||
|
||||
def csrf_input(request):
|
||||
return format_html(
|
||||
'<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
|
||||
@@ -211,19 +296,6 @@ def update_initial_cache(request: object, cache: dict, view: object):
|
||||
return
|
||||
|
||||
|
||||
def structured(self):
|
||||
"""
|
||||
Render the form fields as HTML table rows with Bootstrap styling.
|
||||
"""
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
context = {
|
||||
"form": self,
|
||||
"request": request,
|
||||
}
|
||||
table_html = render_to_string("generic/form.html", context)
|
||||
return table_html
|
||||
|
||||
|
||||
class Reverse:
|
||||
reverse: bool = True
|
||||
page: str = ""
|
||||
@@ -331,3 +403,66 @@ def update_saved_filter_cache(request, cache):
|
||||
},
|
||||
)
|
||||
return cache
|
||||
|
||||
|
||||
def get_nested_field(model_class: models.Model, field_name: str) -> object:
|
||||
"""
|
||||
Recursion function to execute nested field logic
|
||||
"""
|
||||
if "__" in field_name:
|
||||
splits = field_name.split("__", 1)
|
||||
related_model_class = getmodelattribute(
|
||||
model_class,
|
||||
splits[0],
|
||||
).related.related_model
|
||||
return get_nested_field(related_model_class, splits[1])
|
||||
field = getattribute(model_class, field_name)
|
||||
return field
|
||||
|
||||
|
||||
def get_field_class_map(model_class: models.Model, bulk_update_fields: list) -> dict:
|
||||
"""
|
||||
Returns a dictionary mapping field names to their corresponding field classes
|
||||
for a given model class, including related fields(one-to-one).
|
||||
"""
|
||||
field_class_map = {}
|
||||
for field_name in bulk_update_fields:
|
||||
field = get_nested_field(model_class, field_name)
|
||||
field_class_map[field_name] = field.field
|
||||
return field_class_map
|
||||
|
||||
|
||||
def structured(self):
|
||||
"""
|
||||
Render the form fields as HTML table rows with Bootstrap styling.
|
||||
"""
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
context = {
|
||||
"form": self,
|
||||
"request": request,
|
||||
}
|
||||
table_html = render_to_string("generic/form.html", context)
|
||||
return table_html
|
||||
|
||||
|
||||
def value_to_field(field: object, value: list) -> Any:
|
||||
"""
|
||||
return value according to the format of the field
|
||||
"""
|
||||
if isinstance(field, models.ManyToManyField):
|
||||
return [int(val) for val in value]
|
||||
elif isinstance(
|
||||
field,
|
||||
(
|
||||
models.DateField,
|
||||
models.DateTimeField,
|
||||
models.CharField,
|
||||
models.EmailField,
|
||||
models.TextField,
|
||||
models.TimeField,
|
||||
),
|
||||
):
|
||||
value = value[0]
|
||||
return value
|
||||
value = eval(str(value[0]))
|
||||
return value
|
||||
|
||||
@@ -2,12 +2,27 @@
|
||||
horilla_views/forms.py
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.safestring import SafeText
|
||||
from django.utils.translation import gettext_lazy as _trans
|
||||
|
||||
from horilla.horilla_middlewares import _thread_locals
|
||||
from horilla_views import models
|
||||
from horilla_views.cbv_methods import (
|
||||
BOOLEAN_CHOICES,
|
||||
FIELD_WIDGET_MAP,
|
||||
MODEL_FORM_FIELD_MAP,
|
||||
get_field_class_map,
|
||||
structured,
|
||||
value_to_field,
|
||||
)
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
|
||||
class ToggleColumnForm(forms.Form):
|
||||
@@ -75,3 +90,134 @@ class SavedFilterForm(forms.ModelForm):
|
||||
attrs["placeholder"] = "Saved filter title"
|
||||
if self.instance.pk:
|
||||
self.verbose_name = self.instance.title
|
||||
|
||||
|
||||
class DynamicBulkUpdateForm(forms.Form):
|
||||
"""
|
||||
DynamicBulkUpdateForm
|
||||
"""
|
||||
|
||||
verbose_name = _trans("Bulk Update")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
root_model: models.models.Model = None,
|
||||
bulk_update_fields: list = [],
|
||||
ids: list = [],
|
||||
**kwargs
|
||||
):
|
||||
self.ids = ids
|
||||
self.root_model = root_model
|
||||
self.bulk_update_fields = sorted(
|
||||
bulk_update_fields, key=lambda x: x.count("__")
|
||||
)
|
||||
self.structured = structured
|
||||
mappings = get_field_class_map(root_model, bulk_update_fields)
|
||||
self.request = getattribute(_thread_locals, "request")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
for key, val in mappings.items():
|
||||
widget = FIELD_WIDGET_MAP.get(type(val))
|
||||
field = MODEL_FORM_FIELD_MAP.get(type(val))
|
||||
if widget and field:
|
||||
if isinstance(val, models.models.BooleanField):
|
||||
self.fields[key] = forms.ChoiceField(
|
||||
choices=BOOLEAN_CHOICES,
|
||||
widget=widget,
|
||||
label=val.verbose_name.capitalize(),
|
||||
required=False,
|
||||
)
|
||||
continue
|
||||
elif not getattribute(val, "related_model"):
|
||||
self.fields[key] = field(
|
||||
widget=widget,
|
||||
label=val.verbose_name.capitalize(),
|
||||
required=False,
|
||||
)
|
||||
continue
|
||||
queryset = val.related_model.objects.all()
|
||||
self.fields[key] = field(
|
||||
widget=widget,
|
||||
queryset=queryset,
|
||||
label=val.verbose_name,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Bulk save method
|
||||
"""
|
||||
mappings = get_field_class_map(self.root_model, self.bulk_update_fields)
|
||||
data_mapp = {}
|
||||
data_m2m_mapp = {}
|
||||
relation_mapp = {}
|
||||
map_queryset = {}
|
||||
fiels_mapping = {}
|
||||
parent_model = self.root_model
|
||||
for key, val in mappings.items():
|
||||
field = MODEL_FORM_FIELD_MAP.get(type(val))
|
||||
if field:
|
||||
if not fiels_mapping.get(val.model):
|
||||
fiels_mapping[val.model] = {}
|
||||
if not data_m2m_mapp.get(val.model):
|
||||
data_m2m_mapp[val.model] = {}
|
||||
if not data_mapp.get(val.model):
|
||||
data_mapp[val.model] = {}
|
||||
if not relation_mapp.get(val.model):
|
||||
if val.model == self.root_model:
|
||||
relation_mapp[val.model] = "id__in"
|
||||
else:
|
||||
related_key = key.split("__")[-2]
|
||||
field = getattribute(parent_model, related_key)
|
||||
relation_mapp[val.model] = (
|
||||
field.related.field.name
|
||||
+ "__"
|
||||
+ relation_mapp[parent_model]
|
||||
)
|
||||
parent_model = val.model
|
||||
files = self.files.getlist(key)
|
||||
value = self.data.getlist(key)
|
||||
if (not value or not value[0]) and not files:
|
||||
continue
|
||||
key = key.split("__")[-1]
|
||||
model_field = getattribute(val.model, key).field
|
||||
if isinstance(model_field, models.models.ManyToManyField):
|
||||
data_m2m_mapp[val.model][key] = value
|
||||
continue
|
||||
if files and isinstance(
|
||||
model_field,
|
||||
(
|
||||
models.models.FileField,
|
||||
models.models.ImageField,
|
||||
),
|
||||
):
|
||||
file_path = os.path.join(model_field.upload_to, files[0].name)
|
||||
|
||||
data_mapp[val.model][key] = file_path
|
||||
fiels_mapping[val.model][model_field] = files[0]
|
||||
continue
|
||||
data_mapp[val.model][key] = value[0]
|
||||
|
||||
for model, data in data_mapp.items():
|
||||
queryset = model.objects.filter(**{relation_mapp[model]: self.ids})
|
||||
# here fields, files, and related fields-
|
||||
# get updated but need to save the files manually
|
||||
queryset.update(**data)
|
||||
map_queryset[model] = queryset
|
||||
m2m_data = data_m2m_mapp[model]
|
||||
# saving m2m
|
||||
if m2m_data:
|
||||
for field, ids in m2m_data.items():
|
||||
related_objects = getattr(
|
||||
model, field
|
||||
).field.related_model.objects.filter(id__in=ids)
|
||||
for instance in queryset:
|
||||
getattr(instance, field).set(related_objects)
|
||||
for model, files in fiels_mapping.items():
|
||||
if files:
|
||||
for field, file in files.items():
|
||||
file_path = os.path.join(field.upload_to, file.name)
|
||||
default_storage.save(file_path, ContentFile(file.read()))
|
||||
|
||||
messages.success(self.request, _trans("Selected Records updated"))
|
||||
|
||||
@@ -8,10 +8,14 @@ from urllib.parse import parse_qs
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache as CACHE
|
||||
from django.core.paginator import Page
|
||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
||||
from django.shortcuts import render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _trans
|
||||
from django.views.generic import DetailView, FormView, ListView, TemplateView
|
||||
|
||||
from base.methods import closest_numbers, get_key_instances
|
||||
@@ -21,16 +25,18 @@ from horilla.horilla_middlewares import _thread_locals
|
||||
from horilla_views import models
|
||||
from horilla_views.cbv_methods import (
|
||||
get_short_uuid,
|
||||
hx_request_required,
|
||||
paginator_qry,
|
||||
sortby,
|
||||
structured,
|
||||
update_initial_cache,
|
||||
update_saved_filter_cache,
|
||||
)
|
||||
from horilla_views.forms import ToggleColumnForm
|
||||
from horilla_views.forms import DynamicBulkUpdateForm, ToggleColumnForm
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaListView(ListView):
|
||||
"""
|
||||
HorillaListView
|
||||
@@ -87,6 +93,9 @@ class HorillaListView(ListView):
|
||||
export_fields: list = []
|
||||
verbose_name: str = ""
|
||||
|
||||
bulk_update_fields: list = []
|
||||
bulk_template: str = "generic/bulk_form.html"
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
if not self.view_id:
|
||||
self.view_id = get_short_uuid(4)
|
||||
@@ -112,6 +121,78 @@ class HorillaListView(ListView):
|
||||
if column[1] in hidden_fields:
|
||||
self.visible_column.remove(column)
|
||||
|
||||
def bulk_update_accessibility(self) -> bool:
|
||||
"""
|
||||
Accessibility method for bulk update
|
||||
"""
|
||||
return self.request.user.has_perm(
|
||||
f"{self.model._meta.app_label}.change_{self.model.__name__.lower()}"
|
||||
)
|
||||
|
||||
def serve_bulk_form(self, request: HttpRequest) -> HttpResponse:
|
||||
"""
|
||||
Bulk form serve method
|
||||
"""
|
||||
|
||||
if not self.bulk_update_accessibility():
|
||||
return HttpResponse("You dont have permission")
|
||||
form = self.get_bulk_form()
|
||||
form.verbose_name = (
|
||||
form.verbose_name
|
||||
+ f" ({len((eval(request.GET.get('instance_ids','[]'))))} {_trans('Records')})"
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
self.bulk_template,
|
||||
{"form": form, "post_bulk_path": self.post_bulk_path},
|
||||
)
|
||||
|
||||
def handle_bulk_submission(self, request: HttpRequest) -> HttpRequest:
|
||||
"""
|
||||
This method to handle bulk update form submission
|
||||
"""
|
||||
if not self.bulk_update_accessibility():
|
||||
return HttpResponse("You dont have permission")
|
||||
|
||||
instance_ids = request.GET.get("instance_ids", "[]")
|
||||
instance_ids = eval(instance_ids)
|
||||
form = DynamicBulkUpdateForm(
|
||||
request.POST,
|
||||
request.FILES,
|
||||
root_model=self.model,
|
||||
bulk_update_fields=self.bulk_update_fields,
|
||||
ids=instance_ids,
|
||||
)
|
||||
if instance_ids and form.is_valid():
|
||||
form.save()
|
||||
|
||||
script_id = get_short_uuid(length=3, prefix="bulk")
|
||||
return HttpResponse(
|
||||
f"""
|
||||
<script id="{script_id}">
|
||||
$("#{script_id}").closest(".oh-modal--show").removeClass("oh-modal--show");
|
||||
$(".reload-record").click()
|
||||
$("#reloadMessagesButton").click()
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
if not instance_ids:
|
||||
messages.info(request, _trans("No records selected"))
|
||||
return render(
|
||||
request,
|
||||
self.bulk_template,
|
||||
{"form": form, "post_bulk_path": self.post_bulk_path},
|
||||
)
|
||||
|
||||
def get_bulk_form(self):
|
||||
"""
|
||||
Bulk from generating method
|
||||
"""
|
||||
# Bulk update feature
|
||||
return DynamicBulkUpdateForm(
|
||||
root_model=self.model, bulk_update_fields=self.bulk_update_fields
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.queryset:
|
||||
self.queryset = super().get_queryset()
|
||||
@@ -270,6 +351,29 @@ class HorillaListView(ListView):
|
||||
urlpatterns.append(path(self.export_path, self.export_data))
|
||||
context["export_path"] = self.export_path
|
||||
|
||||
if self.bulk_update_fields and self.bulk_update_accessibility():
|
||||
get_bulk_path = (
|
||||
f"get-bulk-update-{self.view_id}-{self.request.session.session_key}/"
|
||||
)
|
||||
post_bulk_path = (
|
||||
f"post-bulk-update-{self.view_id}-{self.request.session.session_key}/"
|
||||
)
|
||||
self.post_bulk_path = post_bulk_path
|
||||
urlpatterns.append(
|
||||
path(
|
||||
get_bulk_path,
|
||||
self.serve_bulk_form,
|
||||
)
|
||||
)
|
||||
urlpatterns.append(
|
||||
path(
|
||||
post_bulk_path,
|
||||
self.handle_bulk_submission,
|
||||
)
|
||||
)
|
||||
context["bulk_update_fields"] = self.bulk_update_fields
|
||||
context["bulk_path"] = get_bulk_path
|
||||
|
||||
return context
|
||||
|
||||
def select_all(self, *args, **kwargs):
|
||||
@@ -380,6 +484,7 @@ class HorillaSectionView(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaDetailedView(DetailView):
|
||||
"""
|
||||
HorillDetailedView
|
||||
@@ -437,6 +542,7 @@ class HorillaDetailedView(DetailView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaTabView(TemplateView):
|
||||
"""
|
||||
HorillaTabView
|
||||
@@ -469,6 +575,7 @@ class HorillaTabView(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaCardView(ListView):
|
||||
"""
|
||||
HorillaCardView
|
||||
@@ -609,7 +716,12 @@ class HorillaCardView(ListView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class ReloadMessages(TemplateView):
|
||||
"""
|
||||
Reload messages
|
||||
"""
|
||||
|
||||
template_name = "generic/messages.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -639,6 +751,7 @@ def save(self: forms.ModelForm, commit=True, *args, **kwargs):
|
||||
return response
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaFormView(FormView):
|
||||
"""
|
||||
HorillaFormView
|
||||
@@ -811,6 +924,7 @@ class HorillaFormView(FormView):
|
||||
return form
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaNavView(TemplateView):
|
||||
"""
|
||||
HorillaNavView
|
||||
@@ -862,6 +976,7 @@ class HorillaNavView(TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(hx_request_required, name="dispatch")
|
||||
class HorillaProfileView(DetailView):
|
||||
"""
|
||||
GenericHorillaProfileView
|
||||
|
||||
4
horilla_views/templates/generic/bulk_form.html
Normal file
4
horilla_views/templates/generic/bulk_form.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<form hx-post="/{{post_bulk_path}}?{{request.GET.urlencode}}" method="post" hx-swap="outerHTML" hx-encoding="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% include "generic/form.html" %}
|
||||
</form>
|
||||
@@ -37,7 +37,7 @@
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="oh-general__tab-target oh-profile-section" id="personal">
|
||||
<div class="oh-general__tab-target oh-profile-section" id="{{form.container_id}}">
|
||||
<div class="oh-profile-section__card row">
|
||||
<div class="row" style="padding-right: 0;">
|
||||
<div class="col-12" style="padding-right: 0;">{{ form.non_field_errors }}</div>
|
||||
@@ -83,3 +83,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("select").on("select2:select", function (e) {
|
||||
$(".leave-message").hide();
|
||||
$(this).closest("select")[0].dispatchEvent(new Event("change"));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -171,6 +171,16 @@
|
||||
</script>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if card_status_indications %}
|
||||
<div class="d-flex flex-row-reverse">
|
||||
{% for indication in card_status_indications %}
|
||||
<span class="m-1" style="cursor: pointer;margin-left: 7px;" {{indication.2|safe}}>
|
||||
<span class="oh-dot oh-dot--small me-1 {{indication.0}}"></span>
|
||||
{{indication.1}}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;">
|
||||
<div align="center">
|
||||
<img src="{% static "images/ui/search.svg" %}" class="oh-404__image" alt="Page not found. 404.">
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-3 " id="enlargeattachmentContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{% load static i18n generic_template_filters %}
|
||||
<div id="{{view_id|safe}}">
|
||||
<div
|
||||
class="oh-modal"
|
||||
id="bulkUpdateModal{{view_id|safe}}"
|
||||
role="dialog"
|
||||
aria-labelledby="bulkUpdateModal{{view_id|safe}}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="oh-modal__dialog" id="bulkUpdateModalBody{{view_id|safe}}"></div>
|
||||
</div>
|
||||
|
||||
{% include "generic/export_fields_modal.html" %}
|
||||
<script>
|
||||
if (!$(".HTV").length) {
|
||||
@@ -45,14 +55,37 @@
|
||||
</span> {% trans "Selected" %}
|
||||
</div>
|
||||
<div
|
||||
id="export_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-info d-none"
|
||||
style="cursor: pointer;"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#exportFields{{view_id|safe}}"
|
||||
id="export_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-info d-none"
|
||||
style="cursor: pointer;"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#exportFields{{view_id|safe}}"
|
||||
>
|
||||
{% trans "Export" %}
|
||||
</div>
|
||||
{% if bulk_path %}
|
||||
<div
|
||||
id="bulk_udate_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-warning d-none"
|
||||
style="cursor: pointer;"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#bulkUpdateModal{{view_id|safe}}"
|
||||
onclick="
|
||||
ids = $('#{{selected_instances_key_id}}').attr('data-ids')
|
||||
$('#bulk_update_get_form{{view_id}}').closest('form').find('[name=instance_ids]').val(ids);
|
||||
$('#bulk_update_get_form{{view_id}}').click()
|
||||
"
|
||||
>
|
||||
{% trans "Update" %}
|
||||
</div>
|
||||
<form
|
||||
hx-get="/{{bulk_path}}"
|
||||
hx-target="#bulkUpdateModalBody{{view_id|safe}}">
|
||||
<input type="hidden" name="instance_ids">
|
||||
<button type="submit" id="bulk_update_get_form{{view_id}}" hidden>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% for filter in stored_filters %}
|
||||
<div class="oh-hover-btn-container"
|
||||
hx-get="{{request.path}}?{{filter.urlencode}}"
|
||||
@@ -393,6 +426,16 @@
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
{% if row_status_indications %}
|
||||
<div class="d-flex flex-row-reverse">
|
||||
{% for indication in row_status_indications %}
|
||||
<span class="m-1" style="cursor: pointer;margin-left: 7px;" {{indication.2|safe}}>
|
||||
<span class="oh-dot oh-dot--small me-1 {{indication.0}}"></span>
|
||||
{{indication.1}}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="oh-wrapper" align="center" style="margin-top: 7vh; margin-bottom:7vh;">
|
||||
<div align="center">
|
||||
<img src="{% static "images/ui/search.svg" %}" class="oh-404__image" alt="Page not found. 404.">
|
||||
|
||||
@@ -131,10 +131,11 @@
|
||||
>
|
||||
<ul class="oh-dropdown__items">
|
||||
{% for action in actions %}
|
||||
{% if action.accessibility|accessibility:instance %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a href="#" class="oh-profile-dropdown-link" {{action.attrs|safe}}>
|
||||
<img
|
||||
src="{{action.icon}}"
|
||||
src="{{action.src}}"
|
||||
style="width: 20px; height: auto"
|
||||
title="{{action.title}}"
|
||||
/>
|
||||
@@ -145,6 +146,7 @@
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -3,102 +3,114 @@
|
||||
{% comment %} {% include "generic/components.html" %} {% endcomment %}
|
||||
{% comment %} {% include "attendance/attendance/attendance_nav.html" %} {% endcomment %}
|
||||
{% load i18n generic_template_filters %}
|
||||
|
||||
<div class="oh-tabs HTV">
|
||||
<script>
|
||||
$("#reloadMessagesButton").click()
|
||||
</script>
|
||||
<div class="HTV"></div>
|
||||
<ul class="oh-tabs__tablist">
|
||||
{% for tab in tabs %}
|
||||
<li
|
||||
class="oh-tabs__tab d-flex {% if forloop.counter == 1 and not active_target %} oh-tabs__tab--active {% endif %}"
|
||||
data-target="#{{view_id}}{{forloop.counter}}"
|
||||
hx-get="{{tab.url}}?{{request.GET.urlencode}}"
|
||||
hx-target="#{{view_id}}{{forloop.counter}}"
|
||||
hx-trigger="load"
|
||||
onclick="switchTab(event)"
|
||||
>
|
||||
{{tab.title}}
|
||||
<div class="d-flex">
|
||||
<div class="oh-tabs__input-badge-container" onclick="event.stopPropagation()">
|
||||
<span
|
||||
class="oh-badge oh-badge--secondary oh-badge--small oh-badge--round ms-2 mr-2"
|
||||
id="badge-{{view_id}}{{forloop.counter}}"
|
||||
{% if tab.badge_label %}
|
||||
data-badge-label="{{tab.badge_label}}"
|
||||
title="0 {{tab.badge_label}}"
|
||||
{% else %}
|
||||
title="0 {% trans "Records" %}"
|
||||
{% endif %}
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</div>
|
||||
{% if tab.actions %}
|
||||
<div onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="oh-btn oh-stop-prop oh-btn--transparent oh-accordion-meta__btn"
|
||||
@click="open = !open"
|
||||
@click.outside="open = false"
|
||||
title="Actions"
|
||||
>
|
||||
<ion-icon
|
||||
name="ellipsis-vertical"
|
||||
role="img"
|
||||
class="md hydrated"
|
||||
aria-label="ellipsis vertical"
|
||||
></ion-icon>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right"
|
||||
x-show="open"
|
||||
style="display: none"
|
||||
>
|
||||
<ul class="oh-dropdown__items">
|
||||
{% for action in tab.actions %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a {{action.attrs|safe}}>{{action.action}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div id="{{view_id}}">
|
||||
<div class="oh-tabs HTV">
|
||||
<script>
|
||||
$("#reloadMessagesButton").click()
|
||||
</script>
|
||||
<div class="HTV"></div>
|
||||
<ul class="oh-tabs__tablist">
|
||||
{% for tab in tabs %}
|
||||
<li
|
||||
class="oh-tabs__tab d-flex {% if forloop.counter == 1 and not active_target %} oh-tabs__tab--active {% endif %}"
|
||||
data-target="#{{view_id}}{{forloop.counter}}"
|
||||
hx-get="{{tab.url}}?{{request.GET.urlencode}}"
|
||||
hx-target="#{{view_id}}{{forloop.counter}}"
|
||||
hx-trigger="load"
|
||||
onclick="switchTab(event)"
|
||||
>
|
||||
{{tab.title}}
|
||||
<div class="d-flex">
|
||||
<div class="oh-tabs__input-badge-container" onclick="event.stopPropagation()">
|
||||
<span
|
||||
class="oh-badge oh-badge--secondary oh-badge--small oh-badge--round ms-2 mr-2"
|
||||
id="badge-{{view_id}}{{forloop.counter}}"
|
||||
{% if tab.badge_label %}
|
||||
data-badge-label="{{tab.badge_label}}"
|
||||
title="0 {{tab.badge_label}}"
|
||||
{% else %}
|
||||
title="0 {% trans "Records" %}"
|
||||
{% endif %}
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</div>
|
||||
{% if tab.actions %}
|
||||
<div onclick="event.stopPropagation()" class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
class="oh-btn oh-stop-prop oh-btn--transparent oh-accordion-meta__btn"
|
||||
@click="open = !open"
|
||||
@click.outside="open = false"
|
||||
title="Actions"
|
||||
>
|
||||
<ion-icon
|
||||
name="ellipsis-vertical"
|
||||
role="img"
|
||||
class="md hydrated"
|
||||
aria-label="ellipsis vertical"
|
||||
></ion-icon>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right"
|
||||
x-show="open"
|
||||
style="display: none"
|
||||
>
|
||||
<ul class="oh-dropdown__items">
|
||||
{% for action in tab.actions %}
|
||||
{% if instance %}
|
||||
{% if action.accessibility|accessibility:instance %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a {{action.attrs|safe}}>{{action.action}}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a {{action.attrs|safe}}>{{action.action}}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="oh-tabs__contents">
|
||||
{% for tab in tabs %}
|
||||
<div
|
||||
class="oh-tabs__content {% if forloop.counter == 1 and not active_target %} oh-tabs__content--active {% endif %}"
|
||||
id="{{view_id}}{{forloop.counter}}"
|
||||
>
|
||||
<div class="animated-background"></div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="oh-tabs__contents">
|
||||
{% for tab in tabs %}
|
||||
<div
|
||||
class="oh-tabs__content {% if forloop.counter == 1 and not active_target %} oh-tabs__content--active {% endif %}"
|
||||
id="{{view_id}}{{forloop.counter}}"
|
||||
>
|
||||
<div class="animated-background"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$("li.oh-tabs__tab").click(function (e) {
|
||||
var target = `li[data-target="${$(this).attr("data-target")}"]`
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "{% url 'active-tab' %}",
|
||||
data: {
|
||||
"path":"{{request.path}}",
|
||||
"target":target,
|
||||
},
|
||||
success: function (response) {
|
||||
<script>
|
||||
$("li.oh-tabs__tab").click(function (e) {
|
||||
var target = `li[data-target="${$(this).attr("data-target")}"]`
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "{% url 'active-tab' %}",
|
||||
data: {
|
||||
"path":"{{request.path}}",
|
||||
"target":target,
|
||||
},
|
||||
success: function (response) {
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
{% if active_target %}
|
||||
$("div.oh-tabs").find(`{{active_target|safe}}`).click();
|
||||
{% endif %}
|
||||
</script>
|
||||
{% if active_target %}
|
||||
$("div.oh-tabs").find(`{{active_target|safe}}`).click();
|
||||
if (!$(".oh-tabs__tab--active").length) {
|
||||
$("#{{view_id}}").find(".oh-tabs__tab").first().click()
|
||||
}
|
||||
{% endif %}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user