[IMP] AUTOMATIONS: Add horilla automations
This commit is contained in:
@@ -3,10 +3,30 @@ horilla_company_manager.py
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import Coroutine, Sequence
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
from horilla.signals import post_bulk_update, pre_bulk_update
|
||||
|
||||
django_filter_update = QuerySet.update
|
||||
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# pre_update signal
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
pre_bulk_update.send(sender=self.model, queryset=self, args=args, kwargs=kwargs)
|
||||
result = django_filter_update(self, *args, **kwargs)
|
||||
# post_update signal
|
||||
post_bulk_update.send(sender=self.model, queryset=self, args=args, kwargs=kwargs)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
setattr(QuerySet, "update", update)
|
||||
|
||||
|
||||
class HorillaCompanyManager(models.Manager):
|
||||
@@ -76,3 +96,8 @@ class HorillaCompanyManager(models.Manager):
|
||||
except:
|
||||
pass
|
||||
return queryset
|
||||
|
||||
def filter(self, *args, **kwargs):
|
||||
queryset = super().filter(*args, **kwargs)
|
||||
setattr(_thread_locals, "queryset_filter", queryset)
|
||||
return queryset
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# from django.db.models.signals import post_save
|
||||
# from notifications.signals import notify
|
||||
# from base.models import *
|
||||
|
||||
# def my_handler(sender, instance, created, **kwargs):
|
||||
# notify.send(instance, verb='was saved')
|
||||
|
||||
# post_save.connect(my_handler, sender=ShiftRequest)
|
||||
|
||||
@@ -156,6 +156,9 @@ class Employee(models.Model):
|
||||
"""
|
||||
return getattr(getattr(self, "employee_work_info", None), "email", self.email)
|
||||
|
||||
def get_email(self):
|
||||
return self.get_mail()
|
||||
|
||||
def get_work_type(self):
|
||||
"""
|
||||
This method is used to return the work type of the employee
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.db import models
|
||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
||||
|
||||
from base.methods import reload_queryset
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
FILTER_FOR_DBFIELD_DEFAULTS[models.ForeignKey][
|
||||
"filter_class"
|
||||
@@ -97,3 +98,25 @@ class HorillaPaginator(Paginator):
|
||||
else self.per_page
|
||||
)
|
||||
return self.page
|
||||
|
||||
|
||||
class HorillaFilterSet(FilterSet):
|
||||
"""
|
||||
HorillaFilterSet
|
||||
"""
|
||||
|
||||
def search_in(self, queryset, name, value):
|
||||
"""
|
||||
Search in generic method for filter field
|
||||
"""
|
||||
search = value.lower()
|
||||
search_field = self.data.get("search_field")
|
||||
if not search_field:
|
||||
search_field = self.filters[name].field_name
|
||||
|
||||
def _icontains(instance):
|
||||
result = str(getattribute(instance, search_field)).lower()
|
||||
return instance.pk if search in result else None
|
||||
|
||||
ids = list(filter(None, map(_icontains, queryset)))
|
||||
return queryset.filter(id__in=ids)
|
||||
|
||||
@@ -15,6 +15,8 @@ INSTALLED_APPS.append("horilla_documents")
|
||||
INSTALLED_APPS.append("haystack")
|
||||
INSTALLED_APPS.append("helpdesk")
|
||||
INSTALLED_APPS.append("offboarding")
|
||||
INSTALLED_APPS.append("horilla_views")
|
||||
INSTALLED_APPS.append("horilla_automations")
|
||||
INSTALLED_APPS.append("auditlog")
|
||||
|
||||
|
||||
|
||||
9
horilla/signals.py
Normal file
9
horilla/signals.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
horilla/signals.py
|
||||
"""
|
||||
|
||||
from django.dispatch import Signal, receiver
|
||||
|
||||
|
||||
pre_bulk_update = Signal()
|
||||
post_bulk_update = Signal()
|
||||
@@ -28,6 +28,8 @@ urlpatterns = [
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("", include("base.urls")),
|
||||
path("", include("horilla_automations.urls")),
|
||||
path("", include("horilla_views.urls")),
|
||||
path("recruitment/", include("recruitment.urls")),
|
||||
path("employee/", include("employee.urls")),
|
||||
path("leave/", include("leave.urls")),
|
||||
|
||||
0
horilla_automations/__init__.py
Normal file
0
horilla_automations/__init__.py
Normal file
12
horilla_automations/admin.py
Normal file
12
horilla_automations/admin.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from horilla_automations.models import MailAutomation
|
||||
|
||||
# Register your models here.
|
||||
|
||||
|
||||
admin.site.register(
|
||||
[
|
||||
MailAutomation,
|
||||
]
|
||||
)
|
||||
36
horilla_automations/apps.py
Normal file
36
horilla_automations/apps.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.apps import AppConfig
|
||||
from horilla_automations.signals import start_automation
|
||||
|
||||
|
||||
class HorillaAutomationConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "horilla_automations"
|
||||
|
||||
def ready(self) -> None:
|
||||
ready = super().ready()
|
||||
try:
|
||||
from recruitment.models import Candidate
|
||||
from horilla_automations.models import MODEL_CHOICES
|
||||
from employee.models import Employee
|
||||
from horilla_automations.methods.methods import get_related_models
|
||||
|
||||
main_models = [Candidate, Employee]
|
||||
for main_model in main_models:
|
||||
related_models = get_related_models(main_model)
|
||||
|
||||
for model in related_models:
|
||||
path = f"{model.__module__}.{model.__name__}"
|
||||
MODEL_CHOICES.append((path, model.__name__))
|
||||
MODEL_CHOICES.append(("employee.models.Employee", "Employee"))
|
||||
MODEL_CHOICES = list(set(MODEL_CHOICES))
|
||||
try:
|
||||
start_automation()
|
||||
except:
|
||||
"""
|
||||
Migrations are not affected yet
|
||||
"""
|
||||
except:
|
||||
"""
|
||||
Models not ready yet
|
||||
"""
|
||||
return ready
|
||||
18
horilla_automations/filters.py
Normal file
18
horilla_automations/filters.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
horilla_automations/filters.py
|
||||
"""
|
||||
|
||||
from horilla.filters import HorillaFilterSet, django_filters
|
||||
from horilla_automations.models import MailAutomation
|
||||
|
||||
|
||||
class AutomationFilter(HorillaFilterSet):
|
||||
"""
|
||||
AutomationFilter
|
||||
"""
|
||||
|
||||
search = django_filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
|
||||
class Meta:
|
||||
model = MailAutomation
|
||||
fields = "__all__"
|
||||
66
horilla_automations/forms.py
Normal file
66
horilla_automations/forms.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
horilla_automations/forms.py
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from django import forms
|
||||
from django.template.loader import render_to_string
|
||||
from horilla_automations.methods.methods import generate_choices
|
||||
from horilla_automations.models import MODEL_CHOICES, MailAutomation
|
||||
from base.forms import ModelForm
|
||||
|
||||
|
||||
class AutomationForm(ModelForm):
|
||||
"""
|
||||
AutomationForm
|
||||
"""
|
||||
|
||||
condition_html = forms.CharField(widget=forms.HiddenInput())
|
||||
condition_querystring = forms.CharField(widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.data:
|
||||
mail_to = []
|
||||
|
||||
initial = []
|
||||
mail_details_choice = []
|
||||
if self.instance.pk:
|
||||
mail_to = generate_choices(self.instance.model)[0]
|
||||
mail_details_choice = generate_choices(self.instance.model)[1]
|
||||
self.fields["mail_to"] = forms.MultipleChoiceField(choices=mail_to)
|
||||
self.fields["mail_details"] = forms.ChoiceField(
|
||||
choices=mail_details_choice,
|
||||
help_text="Fill mail template details(reciever/instance, `self` will be the person who trigger the automation)",
|
||||
)
|
||||
self.fields["mail_to"].initial = initial
|
||||
attrs = self.fields["mail_to"].widget.attrs
|
||||
attrs["class"] = "oh-select oh-select-2 w-100"
|
||||
attrs = self.fields["model"].widget.attrs
|
||||
self.fields["model"].choices = [("", "Select model")] + list(set(MODEL_CHOICES))
|
||||
attrs["onchange"] = "getToMail($(this))"
|
||||
self.fields["mail_template"].empty_label = None
|
||||
attrs = attrs.copy()
|
||||
del attrs["onchange"]
|
||||
self.fields["mail_details"].widget.attrs = attrs
|
||||
if self.instance.pk:
|
||||
self.fields["condition"].initial = self.instance.condition_html
|
||||
self.fields["condition_html"].initial = self.instance.condition_html
|
||||
self.fields["condition_querystring"].initial = (
|
||||
self.instance.condition_querystring
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = MailAutomation
|
||||
fields = "__all__"
|
||||
|
||||
def save(self, commit: bool = ...) -> Any:
|
||||
self.instance: MailAutomation = self.instance
|
||||
condition_querystring = self.cleaned_data["condition_querystring"]
|
||||
condition_html = self.cleaned_data["condition_html"]
|
||||
mail_to = self.data.getlist("mail_to")
|
||||
self.instance.mail_to = str(mail_to)
|
||||
self.instance.mail_details = self.data["mail_details"]
|
||||
self.instance.condition_querystring = condition_querystring
|
||||
self.instance.condition_html = condition_html
|
||||
return super().save(commit)
|
||||
0
horilla_automations/methods/__init__.py
Normal file
0
horilla_automations/methods/__init__.py
Normal file
136
horilla_automations/methods/methods.py
Normal file
136
horilla_automations/methods/methods.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
horilla_automations/methods/methods.py
|
||||
|
||||
"""
|
||||
|
||||
import operator
|
||||
from django.http import QueryDict
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from employee.models import Employee
|
||||
from horilla.models import HorillaModel
|
||||
from recruitment.models import Candidate
|
||||
|
||||
|
||||
def get_related_models(model: HorillaModel) -> list:
|
||||
related_models = []
|
||||
for field in model._meta.get_fields():
|
||||
if field.one_to_many or field.one_to_one or field.many_to_many:
|
||||
related_model = field.related_model
|
||||
related_models.append(related_model)
|
||||
return related_models
|
||||
|
||||
|
||||
def generate_choices(model_path):
|
||||
module_name, class_name = model_path.rsplit(".", 1)
|
||||
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
model_class: Employee = getattr(module, class_name)
|
||||
|
||||
to_fields = []
|
||||
mail_details_choice = []
|
||||
|
||||
for field in list(model_class._meta.fields) + list(model_class._meta.many_to_many):
|
||||
related_model = field.related_model
|
||||
if related_model in [Candidate, Employee]:
|
||||
email_field = (
|
||||
f"{field.name}__get_email",
|
||||
f"{field.verbose_name.capitalize().replace(' id','')} mail field ",
|
||||
)
|
||||
mail_detail = (
|
||||
f"{field.name}__pk",
|
||||
field.verbose_name.capitalize().replace(" id", ""),
|
||||
)
|
||||
if field.related_model == Employee:
|
||||
to_fields.append(
|
||||
(
|
||||
f"{field.name}__employee_work_info__reporting_manager_id__get_email",
|
||||
f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager",
|
||||
)
|
||||
)
|
||||
mail_details_choice.append(
|
||||
(
|
||||
f"{field.name}__employee_work_info__reporting_manager_id__pk",
|
||||
f"{field.verbose_name.capitalize().replace(' id','')}'s reporting manager",
|
||||
)
|
||||
)
|
||||
|
||||
to_fields.append(email_field)
|
||||
mail_details_choice.append(mail_detail)
|
||||
if model_class in [Candidate, Employee]:
|
||||
to_fields.append(
|
||||
(
|
||||
"get_email",
|
||||
f"{model_class.__name__}'s mail",
|
||||
)
|
||||
)
|
||||
mail_details_choice.append(("pk", model_class.__name__))
|
||||
|
||||
to_fields = list(set(to_fields))
|
||||
return to_fields, mail_details_choice, model_class
|
||||
|
||||
|
||||
def get_model_class(model_path):
|
||||
"""
|
||||
method to return the model class from string 'app.models.Model'
|
||||
"""
|
||||
module_name, class_name = model_path.rsplit(".", 1)
|
||||
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
model_class: Employee = getattr(module, class_name)
|
||||
return model_class
|
||||
|
||||
|
||||
operator_map = {
|
||||
"==": operator.eq,
|
||||
"!=": operator.ne,
|
||||
"and": lambda x, y: x and y,
|
||||
"or": lambda x, y: x or y,
|
||||
}
|
||||
|
||||
|
||||
def querydict(query_string):
|
||||
query_dict = QueryDict(query_string)
|
||||
return query_dict
|
||||
|
||||
|
||||
def split_query_string(query_string):
|
||||
"""
|
||||
Split the query string based on the "&logic=" substring
|
||||
"""
|
||||
query_parts = query_string.split("&logic=")
|
||||
result = []
|
||||
|
||||
for i, part in enumerate(query_parts):
|
||||
if i != 0:
|
||||
result.append(querydict("&logic=" + part))
|
||||
else:
|
||||
result.append(querydict(part))
|
||||
return result
|
||||
|
||||
|
||||
def evaluate_condition(value1, operator_str, value2):
|
||||
op_func = operator_map.get(operator_str)
|
||||
if op_func is None:
|
||||
raise ValueError(f"Invalid operator: {operator_str}")
|
||||
return op_func(value1, value2)
|
||||
|
||||
|
||||
def get_related_field_model(model: Employee, field_path):
|
||||
parts = field_path.split("__")
|
||||
for part in parts:
|
||||
# Handle the special case for 'pk'
|
||||
if part == "pk":
|
||||
field = model._meta.pk
|
||||
else:
|
||||
try:
|
||||
field = model._meta.get_field(part)
|
||||
except FieldDoesNotExist:
|
||||
# Handle the case where the field does not exist
|
||||
raise
|
||||
|
||||
if field.is_relation:
|
||||
model = field.related_model
|
||||
else:
|
||||
# If the part is a non-relation field, break the loop
|
||||
break
|
||||
return model
|
||||
83
horilla_automations/methods/serialize.py
Normal file
83
horilla_automations/methods/serialize.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
horilla_automation/methods/serialize.py
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
|
||||
|
||||
def get_related_model_fields(model):
|
||||
fields = []
|
||||
MODEL = model
|
||||
|
||||
class _InstantModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = MODEL
|
||||
fields = "__all__"
|
||||
|
||||
instant_form = _InstantModelForm()
|
||||
for field_name, field in instant_form.fields.items():
|
||||
field_info = {
|
||||
"name": field_name,
|
||||
"type": field.widget.__class__.__name__,
|
||||
"label": field.label,
|
||||
"required": field.required,
|
||||
}
|
||||
fields.append(field_info)
|
||||
if hasattr(field, "queryset"):
|
||||
field_info["options"] = [
|
||||
{"value": choice.pk, "label": str(choice)} for choice in field.queryset
|
||||
]
|
||||
elif hasattr(field, "choices") and field.choices:
|
||||
field_info["options"] = [
|
||||
{"value": choice[0], "label": choice[1]} for choice in field.choices
|
||||
]
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def serialize_form(form, prefix=""):
|
||||
"""
|
||||
serialize_form
|
||||
"""
|
||||
form_fields = form.fields
|
||||
form_structure = []
|
||||
|
||||
for field_name, field in form_fields.items():
|
||||
field_structure = {
|
||||
"name": prefix + field_name,
|
||||
"type": field.widget.__class__.__name__,
|
||||
"label": field.label,
|
||||
"required": field.required,
|
||||
}
|
||||
|
||||
# If the field is a CharField, include the max_length property
|
||||
if isinstance(field, forms.CharField):
|
||||
field_structure["max_length"] = field.max_length
|
||||
|
||||
# If the field is a Select field, include the options
|
||||
if isinstance(field.widget, forms.Select):
|
||||
field_structure["options"] = [
|
||||
{"value": str(key), "label": str(value)} for key, value in field.choices
|
||||
]
|
||||
form_structure.append(field_structure)
|
||||
|
||||
if isinstance(field, forms.ModelChoiceField):
|
||||
related_model = field.queryset.model
|
||||
related_fields = get_related_model_fields(related_model)
|
||||
related_field_structures = []
|
||||
for related_field in related_fields:
|
||||
related_field_structure = {
|
||||
"name": prefix + field_name + "__" + related_field["name"],
|
||||
"type": related_field["type"],
|
||||
"label": related_field["label"].capitalize()
|
||||
+ " | "
|
||||
+ field.label,
|
||||
"required": related_field["required"],
|
||||
}
|
||||
if related_field.get("options"):
|
||||
related_field_structure["options"] = related_field["options"]
|
||||
form_structure.append(related_field_structure)
|
||||
field_structure["related_fields"] = related_field_structures
|
||||
|
||||
return form_structure
|
||||
1
horilla_automations/migrations/__init__.py
Normal file
1
horilla_automations/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
113
horilla_automations/models.py
Normal file
113
horilla_automations/models.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from typing import Any
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from horilla.models import HorillaModel
|
||||
from django.utils.translation import gettext_lazy as _trans
|
||||
from horilla_automations.methods.methods import get_related_models
|
||||
from employee.models import Employee
|
||||
from horilla_views.cbv_methods import render_template
|
||||
from recruitment.models import RecruitmentMailTemplate
|
||||
|
||||
MODEL_CHOICES = []
|
||||
|
||||
CONDITIONS = [
|
||||
("equal", _trans("Equal (==)")),
|
||||
("notequal", _trans("Not Equal (!=)")),
|
||||
("lt", _trans("Less Than (<)")),
|
||||
("gt", _trans("Greater Than (>)")),
|
||||
("le", _trans("Less Than or Equal To (<=)")),
|
||||
("ge", _trans("Greater Than or Equal To (>=)")),
|
||||
("icontains", _trans("Contains")),
|
||||
]
|
||||
|
||||
|
||||
class MailAutomation(HorillaModel):
|
||||
"""
|
||||
MailAutoMation
|
||||
"""
|
||||
|
||||
choices = [
|
||||
("on_create", "On Create"),
|
||||
("on_update", "On Update"),
|
||||
("on_delete", "On Delete"),
|
||||
]
|
||||
title = models.CharField(max_length=50, unique=True)
|
||||
method_title = models.CharField(max_length=50, editable=False)
|
||||
model = models.CharField(max_length=100, choices=MODEL_CHOICES, null=False)
|
||||
mail_to = models.TextField(verbose_name="Mail to")
|
||||
mail_details = models.CharField(
|
||||
max_length=250,
|
||||
help_text="Fill mail template details(reciever/instance, `self` will be the person who trigger the automation)",
|
||||
)
|
||||
mail_detail_choice = models.TextField(default="", editable=False)
|
||||
trigger = models.CharField(max_length=10, choices=choices)
|
||||
# udpate the on_update logic to if and only if when
|
||||
# changes in the previous and current value
|
||||
mail_template = models.ForeignKey(RecruitmentMailTemplate, on_delete=models.CASCADE)
|
||||
template_attachments = models.ManyToManyField(
|
||||
RecruitmentMailTemplate,
|
||||
related_name="template_attachment",
|
||||
blank=True,
|
||||
)
|
||||
condition_html = models.TextField(null=True, editable=False)
|
||||
condition_querystring = models.TextField(null=True, editable=False)
|
||||
|
||||
condition = models.TextField()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
self.method_title = self.title.replace(" ", "_").lower()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
|
||||
def get_avatar(self):
|
||||
"""
|
||||
Method will retun the api to the avatar or path to the profile image
|
||||
"""
|
||||
url = f"https://ui-avatars.com/api/?name={self.title}&background=random"
|
||||
|
||||
return url
|
||||
|
||||
def get_mail_to_display(self):
|
||||
"""
|
||||
method that returns the display value for `mail_to`
|
||||
field
|
||||
"""
|
||||
mail_to = eval(self.mail_to)
|
||||
mappings = []
|
||||
for mapping in mail_to:
|
||||
mapping = mapping.split("__")
|
||||
display = ""
|
||||
for split in mapping:
|
||||
split = split.replace("_id", "").replace("_", " ")
|
||||
split = split.capitalize()
|
||||
display = display + f"{split} >"
|
||||
display = display[:-1]
|
||||
mappings.append(display)
|
||||
return render_template(
|
||||
"horilla_automations/mail_to.html", {"instance": self, "mappings": mappings}
|
||||
)
|
||||
|
||||
def detailed_url(self):
|
||||
return reverse("automation-detailed-view", kwargs={"pk": self.pk})
|
||||
|
||||
def conditions(self):
|
||||
return render_template(
|
||||
"horilla_automations/conditions.html", {"instance": self}
|
||||
)
|
||||
|
||||
def delete_url(self):
|
||||
return reverse("delete-automation", kwargs={"pk": self.pk})
|
||||
|
||||
def edit_url(self):
|
||||
"""
|
||||
Edit url
|
||||
"""
|
||||
return reverse("update-automation", kwargs={"pk": self.pk})
|
||||
|
||||
def trigger_display(self):
|
||||
""""""
|
||||
return self.get_trigger_display()
|
||||
386
horilla_automations/signals.py
Normal file
386
horilla_automations/signals.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
horilla_automation/signals.py
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
import threading
|
||||
import types
|
||||
from django import template
|
||||
from django.db.models.signals import post_save, pre_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db import models
|
||||
from django.core.mail import EmailMessage
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
from horilla.signals import pre_bulk_update, post_bulk_update
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, object_list):
|
||||
# Create a queryset-like object from the list
|
||||
queryset_like_object = cls(model=object_list[0].__class__)
|
||||
queryset_like_object._result_cache = list(object_list)
|
||||
queryset_like_object._prefetch_related_lookups = ()
|
||||
return queryset_like_object
|
||||
|
||||
|
||||
setattr(QuerySet, "from_list", from_list)
|
||||
|
||||
SIGNAL_HANDLERS = []
|
||||
INSTANCE_HANDLERS = []
|
||||
|
||||
|
||||
def start_automation():
|
||||
"""
|
||||
Automation signals
|
||||
"""
|
||||
from horilla_automations.models import MailAutomation
|
||||
from horilla_automations.methods.methods import (
|
||||
split_query_string,
|
||||
get_model_class,
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=MailAutomation)
|
||||
@receiver(post_save, sender=MailAutomation)
|
||||
def automation_pre_create(sender, instance, **kwargs):
|
||||
"""
|
||||
signal method to handle automation post save
|
||||
"""
|
||||
start_connection()
|
||||
track_previous_instance()
|
||||
|
||||
def clear_connection():
|
||||
"""
|
||||
Method to clear signals handlers
|
||||
"""
|
||||
for handler in SIGNAL_HANDLERS:
|
||||
post_save.disconnect(handler, sender=handler.model_class)
|
||||
post_bulk_update.disconnect(handler, sender=handler.model_class)
|
||||
SIGNAL_HANDLERS.clear()
|
||||
|
||||
def create_post_bulk_update_handler(automation, model_class, query_strings):
|
||||
def post_bulk_update_handler(sender, queryset, *args, **kwargs):
|
||||
def _bulk_update_thread_handler(
|
||||
queryset, previous_queryset_copy, automation
|
||||
):
|
||||
request = getattr(queryset, "request", None)
|
||||
|
||||
if request:
|
||||
for index, instance in enumerate(queryset):
|
||||
previous_instance = previous_queryset_copy[index]
|
||||
send_automated_mail(
|
||||
request,
|
||||
False,
|
||||
automation,
|
||||
query_strings,
|
||||
instance,
|
||||
previous_instance,
|
||||
)
|
||||
|
||||
previous_bulk_record = getattr(_thread_locals, "previous_bulk_record", None)
|
||||
previous_queryset = None
|
||||
if previous_bulk_record:
|
||||
previous_queryset = previous_bulk_record["queryset"]
|
||||
previous_queryset_copy = previous_bulk_record["queryset_copy"]
|
||||
|
||||
bulk_thread = threading.Thread(
|
||||
target=_bulk_update_thread_handler,
|
||||
args=(queryset, previous_queryset_copy, automation),
|
||||
)
|
||||
bulk_thread.start()
|
||||
|
||||
func_name = f"{automation.method_title}_post_bulk_signal_handler"
|
||||
|
||||
# Dynamically create a function with a unique name
|
||||
handler = types.FunctionType(
|
||||
post_bulk_update_handler.__code__,
|
||||
globals(),
|
||||
name=func_name,
|
||||
argdefs=post_bulk_update_handler.__defaults__,
|
||||
closure=post_bulk_update_handler.__closure__,
|
||||
)
|
||||
|
||||
# Set additional attributes on the function
|
||||
handler.model_class = model_class
|
||||
handler.automation = automation
|
||||
|
||||
return handler
|
||||
|
||||
def start_connection():
|
||||
"""
|
||||
Method to start signal connection accordingly to the automation
|
||||
"""
|
||||
clear_connection()
|
||||
automations = MailAutomation.objects.filter(is_active=True)
|
||||
for automation in automations:
|
||||
|
||||
condition_querystring = automation.condition_querystring.replace(
|
||||
"automation_multiple_", ""
|
||||
)
|
||||
|
||||
query_strings = split_query_string(condition_querystring)
|
||||
|
||||
model_path = automation.model
|
||||
model_class = get_model_class(model_path)
|
||||
|
||||
handler = create_post_bulk_update_handler(
|
||||
automation, model_class, query_strings
|
||||
)
|
||||
SIGNAL_HANDLERS.append(handler)
|
||||
|
||||
post_bulk_update.connect(handler, sender=model_class)
|
||||
|
||||
def create_signal_handler(name, automation, query_strings):
|
||||
def signal_handler(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Signal handler for post-save events of the model instances.
|
||||
"""
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
previous_record = getattr(_thread_locals, "previous_record", None)
|
||||
previous_instance = None
|
||||
if previous_record:
|
||||
previous_instance = previous_record["instance"]
|
||||
|
||||
args = (
|
||||
request,
|
||||
created,
|
||||
automation,
|
||||
query_strings,
|
||||
instance,
|
||||
previous_instance,
|
||||
)
|
||||
thread = threading.Thread(
|
||||
target=lambda: send_automated_mail(*args),
|
||||
)
|
||||
thread.start()
|
||||
|
||||
signal_handler.__name__ = name
|
||||
signal_handler.model_class = model_class
|
||||
signal_handler.automation = automation
|
||||
return signal_handler
|
||||
|
||||
# Create and connect the signal handler
|
||||
handler_name = f"{automation.method_title}_signal_handler"
|
||||
dynamic_signal_handler = create_signal_handler(
|
||||
handler_name, automation, query_strings
|
||||
)
|
||||
SIGNAL_HANDLERS.append(dynamic_signal_handler)
|
||||
post_save.connect(
|
||||
dynamic_signal_handler, sender=dynamic_signal_handler.model_class
|
||||
)
|
||||
|
||||
def create_pre_bulk_update_handler(automation, model_class):
|
||||
def pre_bulk_update_handler(sender, queryset, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
if request:
|
||||
_thread_locals.previous_bulk_record = {
|
||||
"automation": automation,
|
||||
"queryset": queryset,
|
||||
"queryset_copy": QuerySet.from_list(copy.deepcopy(list(queryset))),
|
||||
}
|
||||
|
||||
func_name = f"{automation.method_title}_pre_bulk_signal_handler"
|
||||
|
||||
# Dynamically create a function with a unique name
|
||||
handler = types.FunctionType(
|
||||
pre_bulk_update_handler.__code__,
|
||||
globals(),
|
||||
name=func_name,
|
||||
argdefs=pre_bulk_update_handler.__defaults__,
|
||||
closure=pre_bulk_update_handler.__closure__,
|
||||
)
|
||||
|
||||
# Set additional attributes on the function
|
||||
handler.model_class = model_class
|
||||
handler.automation = automation
|
||||
|
||||
return handler
|
||||
|
||||
def track_previous_instance():
|
||||
"""
|
||||
method to add signal to track the automations model previous instances
|
||||
"""
|
||||
|
||||
def clear_instance_signal_connection():
|
||||
"""
|
||||
Method to clear instance handler signals
|
||||
"""
|
||||
for handler in INSTANCE_HANDLERS:
|
||||
pre_save.disconnect(handler, sender=handler.model_class)
|
||||
pre_bulk_update.disconnect(handler, sender=handler.model_class)
|
||||
INSTANCE_HANDLERS.clear()
|
||||
|
||||
clear_instance_signal_connection()
|
||||
automations = MailAutomation.objects.filter(is_active=True)
|
||||
for automation in automations:
|
||||
model_class = get_model_class(automation.model)
|
||||
|
||||
handler = create_pre_bulk_update_handler(automation, model_class)
|
||||
INSTANCE_HANDLERS.append(handler)
|
||||
pre_bulk_update.connect(handler, sender=model_class)
|
||||
|
||||
@receiver(pre_save, sender=model_class)
|
||||
def instance_handler(sender, instance, **kwargs):
|
||||
"""
|
||||
Signal handler for pres-save events of the model instances.
|
||||
"""
|
||||
# prevented storing the scheduled activities
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
if instance.pk:
|
||||
# to get the previous instance
|
||||
instance = model_class.objects.filter(id=instance.pk).first()
|
||||
if request:
|
||||
_thread_locals.previous_record = {
|
||||
"automation": automation,
|
||||
"instance": instance,
|
||||
}
|
||||
instance_handler.__name__ = (
|
||||
f"{automation.method_title}_instance_handler"
|
||||
)
|
||||
return instance_handler
|
||||
|
||||
instance_handler.model_class = model_class
|
||||
instance_handler.automation = automation
|
||||
|
||||
INSTANCE_HANDLERS.append(instance_handler)
|
||||
|
||||
track_previous_instance()
|
||||
start_connection()
|
||||
|
||||
|
||||
def send_automated_mail(
|
||||
request,
|
||||
created,
|
||||
automation,
|
||||
query_strings,
|
||||
instance,
|
||||
previous_instance,
|
||||
):
|
||||
from horilla_automations.methods.methods import evaluate_condition, operator_map
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
applicable = False
|
||||
and_exists = False
|
||||
false_exists = False
|
||||
instance_values = []
|
||||
previous_instance_values = []
|
||||
for condition in query_strings:
|
||||
if condition.getlist("condition"):
|
||||
attr = condition.getlist("condition")[0]
|
||||
operator = condition.getlist("condition")[1]
|
||||
value = condition.getlist("condition")[2]
|
||||
|
||||
if value == "on":
|
||||
value = True
|
||||
elif value == "off":
|
||||
value = False
|
||||
instance_value = getattribute(instance, attr)
|
||||
previous_instance_value = getattribute(previous_instance, attr)
|
||||
# The send mail method only trigger when actually any changes
|
||||
# b/w the previous, current instance's `attr` field's values and
|
||||
# if applicable for the automation
|
||||
if getattr(instance_value, "pk", None) and isinstance(
|
||||
instance_value, models.Model
|
||||
):
|
||||
instance_value = str(getattr(instance_value, "pk", None))
|
||||
previous_instance_value = str(
|
||||
getattr(previous_instance_value, "pk", None)
|
||||
)
|
||||
elif isinstance(instance_value, QuerySet):
|
||||
instance_value = list(instance_value.values_list("pk", flat=True))
|
||||
previous_instance_value = list(
|
||||
previous_instance_value.values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
instance_values.append(instance_value)
|
||||
|
||||
previous_instance_values.append(previous_instance_value)
|
||||
|
||||
if not condition.get("logic"):
|
||||
|
||||
applicable = evaluate_condition(instance_value, operator, value)
|
||||
logic = condition.get("logic")
|
||||
if logic:
|
||||
applicable = operator_map[logic](
|
||||
applicable,
|
||||
evaluate_condition(instance_value, operator, value),
|
||||
)
|
||||
if not applicable:
|
||||
false_exists = True
|
||||
if logic == "and":
|
||||
and_exists = True
|
||||
if false_exists and and_exists:
|
||||
applicable = False
|
||||
break
|
||||
if applicable:
|
||||
if created and automation.trigger == "on_create":
|
||||
send_mail(request, automation, instance)
|
||||
elif (automation.trigger == "on_update") and (
|
||||
set(previous_instance_values) != set(instance_values)
|
||||
):
|
||||
|
||||
send_mail(request, automation, instance)
|
||||
|
||||
|
||||
def send_mail(request, automation, instance):
|
||||
"""
|
||||
mail sending method
|
||||
"""
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
from horilla_automations.methods.methods import (
|
||||
get_model_class,
|
||||
get_related_field_model,
|
||||
)
|
||||
from base.methods import generate_pdf
|
||||
from base.backends import ConfiguredEmailBackend
|
||||
from horilla.decorators import logger
|
||||
|
||||
mail_template = automation.mail_template
|
||||
pk = getattribute(instance, automation.mail_details)
|
||||
model_class = get_model_class(automation.model)
|
||||
model_class = get_related_field_model(model_class, automation.mail_details)
|
||||
mail_to_instance = model_class.objects.filter(pk=pk).first()
|
||||
tos = []
|
||||
for mapping in eval(automation.mail_to):
|
||||
tos.append(getattribute(mail_to_instance, mapping))
|
||||
to = tos[:1]
|
||||
cc = tos[1:]
|
||||
email_backend = ConfiguredEmailBackend()
|
||||
host = email_backend.dynamic_username
|
||||
if mail_to_instance and request:
|
||||
attachments = []
|
||||
try:
|
||||
sender = request.user.employee_get
|
||||
except:
|
||||
sender = None
|
||||
for template_attachment in automation.template_attachments.all():
|
||||
template_bdy = template.Template(template_attachment.body)
|
||||
context = template.Context({"instance": mail_to_instance, "self": sender})
|
||||
render_bdy = template_bdy.render(context)
|
||||
attachments.append(
|
||||
(
|
||||
"Document",
|
||||
generate_pdf(render_bdy, {}, path=False, title="Document").content,
|
||||
"application/pdf",
|
||||
)
|
||||
)
|
||||
|
||||
template_bdy = template.Template(mail_template.body)
|
||||
context = template.Context({"instance": mail_to_instance, "self": sender})
|
||||
render_bdy = template_bdy.render(context)
|
||||
email = EmailMessage(automation.title, render_bdy, host, to=to, cc=cc)
|
||||
email.content_subtype = "html"
|
||||
|
||||
email.attachments = attachments
|
||||
|
||||
def _send_mail(email):
|
||||
try:
|
||||
email.send()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=lambda: _send_mail(email),
|
||||
)
|
||||
thread.start()
|
||||
312
horilla_automations/static/automation/automation.js
Normal file
312
horilla_automations/static/automation/automation.js
Normal file
@@ -0,0 +1,312 @@
|
||||
function getToMail(element) {
|
||||
model = element.val();
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "/get-to-mail-field",
|
||||
data: {
|
||||
model: model,
|
||||
},
|
||||
success: function (response) {
|
||||
$(".dynamic-condition-row").remove();
|
||||
select = $("#id_mail_to");
|
||||
select.html("");
|
||||
detailSelect = $("#id_mail_details");
|
||||
detailSelect.html("");
|
||||
mailTo = response.choices;
|
||||
mailDetail = response.mail_details_choice;
|
||||
|
||||
for (let option = 0; option < mailTo.length; option++) {
|
||||
const element = mailTo[option];
|
||||
|
||||
var selected = option === 0; // Set the first option as selected
|
||||
var newOption = new Option(element[1], element[0], selected, selected);
|
||||
select.append(newOption);
|
||||
}
|
||||
for (let option = 0; option < mailDetail.length; option++) {
|
||||
const element = mailDetail[option];
|
||||
|
||||
var selected = option === 0; // Set the first option as selected
|
||||
var newOption = new Option(element[1], element[0], selected, selected);
|
||||
detailSelect.append(newOption);
|
||||
}
|
||||
select.trigger("change");
|
||||
detailSelect.trigger("change");
|
||||
|
||||
table = $("#multipleConditionTable");
|
||||
$("#multipleConditionTable select").select2("destroy");
|
||||
|
||||
totalRows = "C" + (table.find(".dynamic-condition-row").length + 1);
|
||||
|
||||
fieldsChoices = [];
|
||||
$.each(response.serialized_form, function (indexInArray, valueOfElement) {
|
||||
fieldsChoices.push([valueOfElement["name"], valueOfElement["label"]]);
|
||||
});
|
||||
selectField = populateSelect(fieldsChoices, response);
|
||||
tr = `
|
||||
<tr class="dynamic-condition-row">
|
||||
<td class="sn">${totalRows}</td>
|
||||
<td id="conditionalField"></td>
|
||||
<td>
|
||||
<select name="condition" onchange="addSelectedAttr(event)" class="w-100">
|
||||
<option value="==">==</option>
|
||||
<option value="!=">!=</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="condition-value-th"></td>
|
||||
<td>
|
||||
<select name="logic" onchange="addSelectedAttr(event)" class="w-100">
|
||||
<option value="and">And</option>
|
||||
<option value="or">Or</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<div class="oh-btn-group">
|
||||
<button
|
||||
class="oh-btn oh-btn oh-btn--light p-2 w-50"
|
||||
onclick="
|
||||
event.preventDefault();
|
||||
var clonedElement = $(this).closest('tr').clone();
|
||||
totalRows ='C' +( $(this).closest('table').find('.dynamic-condition-row').length + 1);
|
||||
clonedElement.find('.sn').html(totalRows)
|
||||
clonedElement.find('select').parent().find('span').remove()
|
||||
clonedElement.find('select').attr('class','w-100')
|
||||
|
||||
$(this).closest('tr').parent().append(clonedElement)
|
||||
$('#multipleConditionTable').find('select').select2()
|
||||
"
|
||||
>
|
||||
<ion-icon name="copy-outline"></ion-icon>
|
||||
</button>
|
||||
<button
|
||||
class="oh-btn oh-btn oh-btn--light p-2 w-50"
|
||||
onclick="
|
||||
event.preventDefault();
|
||||
$(this).closest('tr').remove();
|
||||
"
|
||||
>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
table.find("tr:last").after(tr);
|
||||
$("#conditionalField").append(selectField);
|
||||
$("#multipleConditionTable select").select2();
|
||||
selectField.trigger("change");
|
||||
selectField.attr("name", "automation_multiple_condition");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getHtml() {
|
||||
var htmlCode = `
|
||||
<form id ="multipleConditionForm">
|
||||
<table id="multipleConditionTable">
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Field</th>
|
||||
<th>Condition</th>
|
||||
<th>Value</th>
|
||||
<th>Logic</th>
|
||||
<th>
|
||||
Action
|
||||
<span title="Reload" onclick="$('[name=model]').change()">
|
||||
<ion-icon name="refresh-circle"></ion-icon>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<script>
|
||||
$("#multipleConditionTable").closest("[contenteditable=true]").removeAttr("contenteditable");
|
||||
</script>
|
||||
`;
|
||||
return $(htmlCode);
|
||||
}
|
||||
|
||||
function populateSelect(data, response) {
|
||||
const selectElement = $(
|
||||
`<select class="w-100" onchange="updateValue($(this));addSelectedAttr(event)" data-response='${JSON.stringify(
|
||||
response.serialized_form
|
||||
).toString()}'></select>`
|
||||
);
|
||||
|
||||
data.forEach((item) => {
|
||||
const $option = $("<option></option>");
|
||||
$option.val(item[0]);
|
||||
$option.text(item[1]);
|
||||
selectElement.append($option);
|
||||
});
|
||||
return selectElement;
|
||||
}
|
||||
|
||||
function updateValue(element) {
|
||||
field = element.val();
|
||||
attr = element.attr("data-response");
|
||||
attr = attr
|
||||
.replace(/[\u0000-\u001F\u007F-\u009F]/g, "")
|
||||
.replace(/\\n/g, "\\\\n")
|
||||
.replace(/\\t/g, "\\\\t");
|
||||
|
||||
response = JSON.parse(attr);
|
||||
|
||||
valueElement = createElement(field, response);
|
||||
element.closest("tr").find(".condition-value-th").html("");
|
||||
element.closest("tr").find(".condition-value-th").html(valueElement);
|
||||
if (valueElement.is("select")) {
|
||||
valueElement.select2();
|
||||
}
|
||||
}
|
||||
|
||||
function createElement(field, serialized_form) {
|
||||
let element;
|
||||
fieldItem = {};
|
||||
|
||||
$.each(serialized_form, function (indexInArray, valueOfElement) {
|
||||
if (valueOfElement.name == field) {
|
||||
fieldItem = valueOfElement;
|
||||
}
|
||||
});
|
||||
|
||||
switch (fieldItem.type) {
|
||||
case "CheckboxInput":
|
||||
element = document.createElement("input");
|
||||
element.type = "checkbox";
|
||||
element.checked = true;
|
||||
element.onchange = function () {
|
||||
if (this.checked) {
|
||||
$(this).attr("checked", true);
|
||||
$(this).val("on");
|
||||
} else {
|
||||
$(this).attr("checked", false);
|
||||
$(this).val("off");
|
||||
}
|
||||
};
|
||||
element.name = "automation_multiple_condition";
|
||||
element.className = "oh-switch__checkbox oh-switch__checkbox";
|
||||
// Create the wrapping div
|
||||
const wrapperDiv = document.createElement("div");
|
||||
wrapperDiv.className = "oh-switch";
|
||||
wrapperDiv.style.width = "30px";
|
||||
// Append the checkbox input to the div
|
||||
wrapperDiv.appendChild(element);
|
||||
$(element).change();
|
||||
element = wrapperDiv;
|
||||
break;
|
||||
|
||||
case "Select":
|
||||
case "SelectMultiple":
|
||||
element = document.createElement("select");
|
||||
if (fieldItem.type === "SelectMultiple") {
|
||||
element.multiple = true;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
addSelectedAttr(event);
|
||||
};
|
||||
fieldItem.options.forEach((optionValue) => {
|
||||
if (optionValue.value) {
|
||||
const option = document.createElement("option");
|
||||
option.value = optionValue.value;
|
||||
option.textContent = optionValue.label;
|
||||
element.appendChild(option);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "Textarea":
|
||||
element = document.createElement("textarea");
|
||||
element.style = `
|
||||
height: 29px !important;
|
||||
margin-top: 5px;
|
||||
`;
|
||||
element.className = "oh-input w-100";
|
||||
if (fieldItem.max_length) {
|
||||
element.maxLength = fieldItem.max_length;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
$(this).html($(this).val());
|
||||
};
|
||||
break;
|
||||
case "TextInput":
|
||||
element = document.createElement("input");
|
||||
element.type = "text";
|
||||
element.style = `
|
||||
height: 30px !important;
|
||||
`;
|
||||
element.className = "oh-input w-100";
|
||||
if (fieldItem.max_length) {
|
||||
element.maxLength = fieldItem.max_length;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
$(this).attr("value", $(this).val());
|
||||
};
|
||||
break;
|
||||
case "EmailInput":
|
||||
element = document.createElement("input");
|
||||
element.type = "email";
|
||||
element.style = `
|
||||
height: 30px !important;
|
||||
`;
|
||||
element.className = "oh-input w-100";
|
||||
if (fieldItem.max_length) {
|
||||
element.maxLength = fieldItem.max_length;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
$(this).attr("value", $(this).val());
|
||||
};
|
||||
break;
|
||||
case "NumberInput":
|
||||
element = document.createElement("input");
|
||||
element.type = "number";
|
||||
element.style = `
|
||||
height: 30px !important;
|
||||
`;
|
||||
element.className = "oh-input w-100";
|
||||
if (fieldItem.max_length) {
|
||||
element.maxLength = fieldItem.max_length;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
$(this).attr("value", $(this).val());
|
||||
};
|
||||
break;
|
||||
default:
|
||||
element = document.createElement("input");
|
||||
element.type = "text";
|
||||
element.style = `
|
||||
height: 30px !important;
|
||||
`;
|
||||
element.className = "oh-input w-100";
|
||||
if (fieldItem.max_length) {
|
||||
element.maxLength = fieldItem.max_length;
|
||||
}
|
||||
element.onchange = function (event) {
|
||||
$(this).attr("value", $(this).val());
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (element) {
|
||||
element.name = "automation_multiple_condition";
|
||||
if (fieldItem.required) {
|
||||
element.required = true;
|
||||
}
|
||||
|
||||
// Create label
|
||||
const label = document.createElement("label");
|
||||
label.textContent = fieldItem.label;
|
||||
label.htmlFor = "automation_multiple_condition";
|
||||
|
||||
return $(element);
|
||||
}
|
||||
}
|
||||
|
||||
function addSelectedAttr(event) {
|
||||
const options = Array.from(event.target.options);
|
||||
options.forEach((option) => {
|
||||
if (option.selected) {
|
||||
option.setAttribute("selected", "selected");
|
||||
} else {
|
||||
option.removeAttribute("selected");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div id="formContainer">
|
||||
{% include "generic/horilla_form.html" %}
|
||||
</div>
|
||||
<script>
|
||||
$("form#{{view_id}} button").click(function (e) {
|
||||
const form = document.getElementById('multipleConditionForm');
|
||||
const elements = form.elements;
|
||||
const queryString = Array.from(elements)
|
||||
.filter(element => element.name && !element.disabled)
|
||||
.map(element => encodeURIComponent(element.name) + '=' + encodeURIComponent(element.value))
|
||||
.join('&');
|
||||
$("form#{{view_id}} [name=condition_querystring]").val(queryString);
|
||||
html = $(".note-editable #multipleConditionForm").html()
|
||||
$("form#{{view_id}} [name=condition_html]").val(html);
|
||||
|
||||
});
|
||||
function getToMail(element) {
|
||||
model = $(this).val();
|
||||
}
|
||||
$("#dynamic_field_condition").parent().removeClass("col-md-6");
|
||||
// summernote
|
||||
$("#dynamic_field_condition textarea")
|
||||
.summernote({
|
||||
height: 100,
|
||||
toolbar: false,
|
||||
})
|
||||
.summernote("code", getHtml());
|
||||
|
||||
{% if form.instance.pk %}
|
||||
$("#id_mail_to").val({{form.instance.mail_to|safe}}).change()
|
||||
$("#dynamic_field_condition .note-editable").html($("<form id='multipleConditionForm'></form>"));
|
||||
$("#dynamic_field_condition .note-editable #multipleConditionForm").html($(`{{form.instance.condition_html|safe}}`))
|
||||
$(".note-editable select").parent().find("span.select2").remove()
|
||||
$(".note-editable select").parent().find(".select2-hidden-accessible").removeClass("select2-hidden-accessible")
|
||||
$(".note-editable select").parent().find("select").select2();
|
||||
{% endif %}
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1 @@
|
||||
{{instance.condition_html|safe|escape }}
|
||||
@@ -0,0 +1,5 @@
|
||||
<ol>
|
||||
{% for to in mappings %}
|
||||
<li>{{to}}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
@@ -0,0 +1,17 @@
|
||||
{% include "generic/horilla_section.html" %}
|
||||
<script>
|
||||
function confirm(message, url) {
|
||||
Swal.fire({
|
||||
text: message,
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#008000",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Confirm",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
3
horilla_automations/tests.py
Normal file
3
horilla_automations/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
51
horilla_automations/urls.py
Normal file
51
horilla_automations/urls.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
horilla_automations/urls.py
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from horilla_automations.views import views
|
||||
from horilla_automations.views import cbvs
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"mail-automations",
|
||||
cbvs.AutomationSectionView.as_view(),
|
||||
name="mail-automations",
|
||||
),
|
||||
path(
|
||||
"mail-automations-nav",
|
||||
cbvs.AutomationNavView.as_view(),
|
||||
name="mail-automations-nav",
|
||||
),
|
||||
path(
|
||||
"create-automation",
|
||||
cbvs.AutomationFormView.as_view(),
|
||||
name="create-automation",
|
||||
),
|
||||
path(
|
||||
"update-automation/<int:pk>/",
|
||||
cbvs.AutomationFormView.as_view(),
|
||||
name="update-automation",
|
||||
),
|
||||
path(
|
||||
"mail-automations-list-view",
|
||||
cbvs.AutomationListView.as_view(),
|
||||
name="mail-automations-list-view",
|
||||
),
|
||||
path(
|
||||
"get-to-mail-field",
|
||||
views.get_to_field,
|
||||
name="get-to-mail-field",
|
||||
),
|
||||
path(
|
||||
"automation-detailed-view/<int:pk>/",
|
||||
cbvs.AutomationDetailedView.as_view(),
|
||||
name="automation-detailed-view",
|
||||
),
|
||||
path(
|
||||
"delete-automation/<int:pk>/",
|
||||
views.delete_automation,
|
||||
name="delete-automation",
|
||||
),
|
||||
]
|
||||
0
horilla_automations/views/__init__.py
Normal file
0
horilla_automations/views/__init__.py
Normal file
167
horilla_automations/views/cbvs.py
Normal file
167
horilla_automations/views/cbvs.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
horilla_automations/views/cbvs.py
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from horilla_automations.filters import AutomationFilter
|
||||
from horilla_automations.forms import AutomationForm
|
||||
from horilla_views.generic.cbv import views
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _trans
|
||||
from django.contrib import messages
|
||||
from horilla_automations import models
|
||||
|
||||
|
||||
class AutomationSectionView(views.HorillaSectionView):
|
||||
"""
|
||||
AutomationSectionView
|
||||
"""
|
||||
|
||||
nav_url = reverse_lazy("mail-automations-nav")
|
||||
view_url = reverse_lazy("mail-automations-list-view")
|
||||
view_container_id = "listContainer"
|
||||
|
||||
script_static_paths = [
|
||||
"static/automation/automation.js",
|
||||
]
|
||||
|
||||
template_name = "horilla_automations/section_view.html"
|
||||
|
||||
|
||||
class AutomationNavView(views.HorillaNavView):
|
||||
"""
|
||||
AutomationNavView
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.create_attrs = f"""
|
||||
hx-get="{reverse_lazy("create-automation")}"
|
||||
hx-target="#genericModalBody"
|
||||
data-target="#genericModal"
|
||||
data-toggle="oh-modal-toggle"
|
||||
"""
|
||||
|
||||
nav_title = _trans("Automations")
|
||||
search_url = reverse_lazy("mail-automations-list-view")
|
||||
search_swap_target = "#listContainer"
|
||||
|
||||
|
||||
class AutomationFormView(views.HorillaFormView):
|
||||
"""
|
||||
AutomationFormView
|
||||
"""
|
||||
|
||||
form_class = AutomationForm
|
||||
model = models.MailAutomation
|
||||
new_display_title = _trans("New Automation")
|
||||
template_name = "horilla_automations/automation_form.html"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
instance = models.MailAutomation.objects.filter(pk=self.kwargs["pk"]).first()
|
||||
kwargs["instance"] = instance
|
||||
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form: AutomationForm) -> views.HttpResponse:
|
||||
if form.is_valid():
|
||||
message = "New automation added"
|
||||
if form.instance.pk:
|
||||
message = "Automation updated"
|
||||
form.save()
|
||||
|
||||
messages.success(self.request, _trans(message))
|
||||
return self.HttpResponse()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class AutomationListView(views.HorillaListView):
|
||||
"""
|
||||
AutomationListView
|
||||
"""
|
||||
|
||||
row_attrs = """
|
||||
hx-get="{detailed_url}?instance_ids={ordered_ids}"
|
||||
hx-target="#genericModalBody"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#genericModal"
|
||||
"""
|
||||
|
||||
model = models.MailAutomation
|
||||
search_url = reverse_lazy("mail-automations-list-view")
|
||||
filter_class = AutomationFilter
|
||||
|
||||
actions = [
|
||||
{
|
||||
"action": "Edit",
|
||||
"icon": "create-outline",
|
||||
"attrs": """
|
||||
class="oh-btn oh-btn--light-bkg w-100"
|
||||
hx-get="{edit_url}"
|
||||
hx-target="#genericModalBody"
|
||||
data-target="#genericModal"
|
||||
data-toggle="oh-modal-toggle"
|
||||
""",
|
||||
},
|
||||
{
|
||||
"action": "Delete",
|
||||
"icon": "trash-outline",
|
||||
"attrs": """
|
||||
class="oh-btn oh-btn--light-bkg w-100 tex-danger"
|
||||
onclick="
|
||||
event.stopPropagation();
|
||||
confirm('Do you want to delete the automation?','{delete_url}')
|
||||
"
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
||||
columns = [
|
||||
("Title", "title"),
|
||||
("Model", "model"),
|
||||
("Email Mapping", "get_mail_to_display"),
|
||||
]
|
||||
|
||||
|
||||
class AutomationDetailedView(views.HorillaDetailedView):
|
||||
"""
|
||||
AutomationDetailedView
|
||||
"""
|
||||
|
||||
model = models.MailAutomation
|
||||
title = "Detailed View"
|
||||
header = {
|
||||
"title": "title",
|
||||
"subtitle": "title",
|
||||
"avatar": "get_avatar",
|
||||
}
|
||||
body = [
|
||||
("Model", "model"),
|
||||
("Mail Templates", "mail_template"),
|
||||
("Mail To", "get_mail_to_display"),
|
||||
("Trigger", "trigger_display"),
|
||||
]
|
||||
actions = [
|
||||
{
|
||||
"action": "Edit",
|
||||
"icon": "create-outline",
|
||||
"attrs": """
|
||||
hx-get="{edit_url}"
|
||||
hx-target="#genericModalBody"
|
||||
data-toggle="oh-modal-toggle"
|
||||
data-target="#genericModal"
|
||||
class="oh-btn oh-btn--info w-50"
|
||||
""",
|
||||
},
|
||||
{
|
||||
"action": "Delete",
|
||||
"icon": "trash-outline",
|
||||
"attrs": """
|
||||
class="oh-btn oh-btn--danger w-50"
|
||||
onclick="
|
||||
confirm('Do you want to delete the automation?','{delete_url}')
|
||||
"
|
||||
""",
|
||||
},
|
||||
]
|
||||
60
horilla_automations/views/views.py
Normal file
60
horilla_automations/views/views.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
horilla_automation/views/views.py
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.http import JsonResponse
|
||||
from horilla.decorators import login_required
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from horilla_automations.methods.methods import generate_choices
|
||||
from horilla_automations.models import MailAutomation
|
||||
from horilla.decorators import permission_required
|
||||
from recruitment.models import Candidate
|
||||
from employee.models import Employee
|
||||
|
||||
from horilla_automations.methods.serialize import serialize_form
|
||||
|
||||
|
||||
@login_required
|
||||
def get_to_field(request):
|
||||
"""
|
||||
This method is to render `mail to` fields
|
||||
"""
|
||||
model_path = request.GET["model"]
|
||||
to_fields, mail_details_choice, model_class = generate_choices(model_path)
|
||||
|
||||
class InstantModelForm(forms.ModelForm):
|
||||
"""
|
||||
InstantModelForm
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = model_class
|
||||
fields = "__all__"
|
||||
|
||||
serialized_form = serialize_form(InstantModelForm(), "automation_multiple_")
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"choices": to_fields,
|
||||
"mail_details_choice": mail_details_choice,
|
||||
"serialized_form": serialized_form,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("horilla_automation")
|
||||
def delete_automation(request, pk):
|
||||
"""
|
||||
Automation delete view
|
||||
"""
|
||||
try:
|
||||
MailAutomation.objects.get(id=pk).delete()
|
||||
messages.success(request, "Automation deleted")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
messages.error(request, "Something went wrong")
|
||||
return redirect(reverse("mail-automations"))
|
||||
0
horilla_views/__init__.py
Normal file
0
horilla_views/__init__.py
Normal file
10
horilla_views/admin.py
Normal file
10
horilla_views/admin.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from horilla_views.models import (
|
||||
ToggleColumn,
|
||||
ParentModel,
|
||||
childModel,
|
||||
ActiveTab,
|
||||
ActiveGroup,
|
||||
)
|
||||
|
||||
admin.site.register([ToggleColumn, ParentModel, childModel, ActiveTab, ActiveGroup])
|
||||
6
horilla_views/apps.py
Normal file
6
horilla_views/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class HorillaViewsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'horilla_views'
|
||||
292
horilla_views/cbv_methods.py
Normal file
292
horilla_views/cbv_methods.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
horilla/cbv_methods.py
|
||||
"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
import uuid
|
||||
from venv import logger
|
||||
from django import template
|
||||
from django.shortcuts import redirect, render
|
||||
from django.template import loader
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.defaultfilters import register
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.core.paginator import Paginator
|
||||
from django.middleware.csrf import get_token
|
||||
from django.utils.html import format_html
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.safestring import SafeString
|
||||
|
||||
from horilla import settings
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
|
||||
|
||||
def decorator_with_arguments(decorator):
|
||||
"""
|
||||
Decorator that allows decorators to accept arguments and keyword arguments.
|
||||
|
||||
Args:
|
||||
decorator (function): The decorator function to be wrapped.
|
||||
|
||||
Returns:
|
||||
function: The wrapper function.
|
||||
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wrapper function that captures the arguments and keyword arguments.
|
||||
|
||||
Args:
|
||||
*args: Variable length argument list.
|
||||
**kwargs: Arbitrary keyword arguments.
|
||||
|
||||
Returns:
|
||||
function: The inner wrapper function.
|
||||
|
||||
"""
|
||||
|
||||
def inner_wrapper(func):
|
||||
"""
|
||||
Inner wrapper function that applies the decorator to the function.
|
||||
|
||||
Args:
|
||||
func (function): The function to be decorated.
|
||||
|
||||
Returns:
|
||||
function: The decorated function.
|
||||
|
||||
"""
|
||||
return decorator(func, *args, **kwargs)
|
||||
|
||||
return inner_wrapper
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def login_required(view_func):
|
||||
def wrapped_view(self, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request")
|
||||
if not getattr(self, "request", None):
|
||||
self.request = request
|
||||
path = request.path
|
||||
res = path.split("/", 2)[1].capitalize().replace("-", " ").upper()
|
||||
if res == "PMS":
|
||||
res = "Performance"
|
||||
request.session["title"] = res
|
||||
if path == "" or path == "/":
|
||||
request.session["title"] = "Dashboard".upper()
|
||||
if not request.user.is_authenticated:
|
||||
login_url = reverse("login")
|
||||
params = urlencode(request.GET)
|
||||
url = f"{login_url}?next={request.path}"
|
||||
if params:
|
||||
url += f"&{params}"
|
||||
return redirect(url)
|
||||
try:
|
||||
func = view_func(self, request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
if not settings.DEBUG:
|
||||
return render(request, "went_wrong.html")
|
||||
return view_func(self, *args, **kwargs)
|
||||
return func
|
||||
|
||||
return wrapped_view
|
||||
|
||||
|
||||
@decorator_with_arguments
|
||||
def permission_required(function, perm):
|
||||
def _function(self, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request")
|
||||
if not getattr(self, "request", None):
|
||||
self.request = request
|
||||
|
||||
if request.user.has_perm(perm):
|
||||
return function(self, *args, **kwargs)
|
||||
else:
|
||||
messages.info(request, "You dont have permission.")
|
||||
previous_url = request.META.get("HTTP_REFERER", "/")
|
||||
key = "HTTP_HX_REQUEST"
|
||||
if key in request.META.keys():
|
||||
return render(request, "decorator_404.html")
|
||||
script = f'<script>window.location.href = "{previous_url}"</script>'
|
||||
return HttpResponse(script)
|
||||
|
||||
return _function
|
||||
|
||||
|
||||
|
||||
def csrf_input(request):
|
||||
return format_html(
|
||||
'<input type="hidden" name="csrfmiddlewaretoken" value="{}">',
|
||||
get_token(request),
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def csrf_token(context):
|
||||
"""
|
||||
to access csrf token inside the render_template method
|
||||
"""
|
||||
request = context["request"]
|
||||
csrf_input_lazy = lazy(csrf_input, SafeString, str)
|
||||
return csrf_input_lazy(request)
|
||||
|
||||
|
||||
def get_all_context_variables(request) -> dict:
|
||||
"""
|
||||
This method will return dictionary format of context processors
|
||||
"""
|
||||
if getattr(request, "all_context_variables", None) is None:
|
||||
all_context_variables = {}
|
||||
for processor_path in settings.TEMPLATES[0]["OPTIONS"]["context_processors"]:
|
||||
module_path, func_name = processor_path.rsplit(".", 1)
|
||||
module = __import__(module_path, fromlist=[func_name])
|
||||
func = getattr(module, func_name)
|
||||
context = func(request)
|
||||
all_context_variables.update(context)
|
||||
all_context_variables["csrf_token"] = csrf_token(all_context_variables)
|
||||
request.all_context_variables = all_context_variables
|
||||
return request.all_context_variables
|
||||
|
||||
|
||||
def render_template(
|
||||
path: str,
|
||||
context: dict,
|
||||
decoding: str = "utf-8",
|
||||
status: int = None,
|
||||
_using=None,
|
||||
) -> str:
|
||||
"""
|
||||
This method is used to render HTML text with context.
|
||||
"""
|
||||
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
context.update(get_all_context_variables(request))
|
||||
template_loader = loader.get_template(path)
|
||||
template_body = template_loader.template.source
|
||||
template_bdy = template.Template(template_body)
|
||||
context_instance = template.Context(context)
|
||||
rendered_content = template_bdy.render(context_instance)
|
||||
return HttpResponse(rendered_content, status=status).content.decode(decoding)
|
||||
|
||||
|
||||
def paginator_qry(qryset, page_number, records_per_page=50):
|
||||
"""
|
||||
This method is used to paginate queryset
|
||||
"""
|
||||
paginator = Paginator(qryset, records_per_page)
|
||||
qryset = paginator.get_page(page_number)
|
||||
return qryset
|
||||
|
||||
|
||||
def get_short_uuid(length: int, prefix: str = "hlv"):
|
||||
"""
|
||||
Short uuid generating method
|
||||
"""
|
||||
uuid_str = str(uuid.uuid4().hex)
|
||||
return prefix + str(uuid_str[:length]).replace("-", "")
|
||||
|
||||
|
||||
def update_initial_cache(request: object, cache: dict, view: object):
|
||||
if cache.get(request.session.session_key):
|
||||
cache[request.session.session_key].update({view: {}})
|
||||
return
|
||||
cache.update({request.session.session_key: {view: {}}})
|
||||
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 = ""
|
||||
|
||||
|
||||
cache = {}
|
||||
|
||||
|
||||
def sortby(
|
||||
query_dict, queryset, key: str, page: str = "page", is_first_sort: bool = False
|
||||
):
|
||||
"""
|
||||
New simplified method to sort the queryset/lists
|
||||
"""
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
sort_key = query_dict[key]
|
||||
if not cache.get(request.session.session_key):
|
||||
cache[request.session.session_key] = Reverse()
|
||||
cache[request.session.session_key].page = (
|
||||
"1" if not query_dict.get(page) else query_dict.get(page)
|
||||
)
|
||||
reverse = cache[request.session.session_key].reverse
|
||||
none_ids = []
|
||||
|
||||
def _sortby(object):
|
||||
result = getattribute(object, attr=sort_key)
|
||||
if result is None:
|
||||
none_ids.append(object.pk)
|
||||
return result
|
||||
|
||||
order = not reverse
|
||||
current_page = query_dict.get(page)
|
||||
if current_page or is_first_sort:
|
||||
order = not order
|
||||
if (
|
||||
cache[request.session.session_key].page == current_page
|
||||
and not is_first_sort
|
||||
):
|
||||
order = not order
|
||||
cache[request.session.session_key].page = current_page
|
||||
try:
|
||||
queryset = sorted(queryset, key=_sortby, reverse=order)
|
||||
except TypeError:
|
||||
none_queryset = list(queryset.filter(id__in=none_ids))
|
||||
queryset = sorted(queryset.exclude(id__in=none_ids), key=_sortby, reverse=order)
|
||||
queryset = queryset + none_queryset
|
||||
|
||||
cache[request.session.session_key].reverse = order
|
||||
order = "asc" if not order else "desc"
|
||||
setattr(request, "sort_order", order)
|
||||
setattr(request, "sort_key", sort_key)
|
||||
return queryset
|
||||
|
||||
|
||||
def update_saved_filter_cache(request, cache):
|
||||
"""
|
||||
Method to save filter on cache
|
||||
"""
|
||||
if cache.get(request.session.session_key):
|
||||
cache[request.session.session_key].update(
|
||||
{
|
||||
"path": request.path,
|
||||
"query_dict": request.GET,
|
||||
"request": request,
|
||||
}
|
||||
)
|
||||
return cache
|
||||
cache.update(
|
||||
{
|
||||
request.session.session_key: {
|
||||
"path": request.path,
|
||||
"query_dict": request.GET,
|
||||
"request": request,
|
||||
}
|
||||
}
|
||||
)
|
||||
return cache
|
||||
35
horilla_views/forms.py
Normal file
35
horilla_views/forms.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
horilla_views/forms.py
|
||||
"""
|
||||
|
||||
from django import forms
|
||||
from django.utils.safestring import SafeText
|
||||
from django.template.loader import render_to_string
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
|
||||
|
||||
class ToggleColumnForm(forms.Form):
|
||||
"""
|
||||
Toggle column form
|
||||
"""
|
||||
|
||||
def __init__(self, columns, hidden_fields: list, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request", {})
|
||||
self.request = request
|
||||
super().__init__(*args, **kwargs)
|
||||
for column in columns:
|
||||
initial = True
|
||||
if column[1] in hidden_fields:
|
||||
initial = False
|
||||
|
||||
self.fields[column[1]] = forms.BooleanField(
|
||||
label=column[0], initial=initial
|
||||
)
|
||||
|
||||
def as_list(self) -> SafeText:
|
||||
"""
|
||||
Render the form fields as HTML table rows with.
|
||||
"""
|
||||
context = {"form": self, "request": self.request}
|
||||
table_html = render_to_string("generic/as_list.html", context)
|
||||
return table_html
|
||||
0
horilla_views/generic/__init__.py
Normal file
0
horilla_views/generic/__init__.py
Normal file
0
horilla_views/generic/cbv/__init__.py
Normal file
0
horilla_views/generic/cbv/__init__.py
Normal file
681
horilla_views/generic/cbv/views.py
Normal file
681
horilla_views/generic/cbv/views.py
Normal file
@@ -0,0 +1,681 @@
|
||||
"""
|
||||
horilla/generic/views.py
|
||||
"""
|
||||
|
||||
import json
|
||||
from django import forms
|
||||
from django.http import HttpRequest, HttpResponse, QueryDict
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from typing import Any
|
||||
from django.urls import resolve
|
||||
from urllib.parse import parse_qs
|
||||
from django.core.paginator import Page
|
||||
from django.views.generic import ListView, DetailView, TemplateView, FormView
|
||||
from attendance.methods.group_by import group_by_queryset
|
||||
from base.methods import (
|
||||
closest_numbers,
|
||||
get_key_instances,
|
||||
)
|
||||
from horilla.filters import FilterSet
|
||||
from horilla_views import models
|
||||
from horilla_views.cbv_methods import (
|
||||
get_short_uuid,
|
||||
paginator_qry,
|
||||
update_initial_cache,
|
||||
sortby,
|
||||
update_saved_filter_cache,
|
||||
)
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
from horilla_views.cbv_methods import structured
|
||||
from horilla_views.forms import ToggleColumnForm
|
||||
from horilla_views.templatetags.generic_template_filters import getattribute
|
||||
|
||||
|
||||
cache = {}
|
||||
saved_filters = {}
|
||||
|
||||
|
||||
class HorillaListView(ListView):
|
||||
"""
|
||||
HorillaListView
|
||||
"""
|
||||
|
||||
filter_class: FilterSet = None
|
||||
|
||||
view_id: str = """"""
|
||||
|
||||
export_file_name: str = None
|
||||
|
||||
template_name: str = "generic/horilla_list.html"
|
||||
context_object_name = "queryset"
|
||||
# column = [("Verbose Name","field_name","avatar_mapping")], opt: avatar_mapping
|
||||
columns: list = []
|
||||
search_url: str = ""
|
||||
bulk_select_option: bool = True
|
||||
|
||||
action_method: str = """"""
|
||||
actions: list = []
|
||||
|
||||
option_method: str = ""
|
||||
options: list = []
|
||||
row_attrs: str = """"""
|
||||
row_status_class: str = """"""
|
||||
row_status_indications: list = []
|
||||
|
||||
sortby_key: str = "sortby"
|
||||
sortby_mapping: list = []
|
||||
|
||||
show_filter_tags: bool = True
|
||||
filter_keys_to_remove: list = []
|
||||
|
||||
records_per_page: int = 50
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.view_id = get_short_uuid(4)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaListView)
|
||||
|
||||
# hidden columns configuration
|
||||
existing_instance = models.ToggleColumn.objects.filter(
|
||||
user_id=request.user, path=request.path_info
|
||||
).first()
|
||||
|
||||
hidden_fields = (
|
||||
[] if not existing_instance else existing_instance.excluded_columns
|
||||
)
|
||||
|
||||
self.visible_column = self.columns.copy()
|
||||
|
||||
self.toggle_form = ToggleColumnForm(self.columns, hidden_fields)
|
||||
for column in self.columns:
|
||||
if column[1] in hidden_fields:
|
||||
self.visible_column.remove(column)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
if self.filter_class:
|
||||
query_dict = self.request.GET
|
||||
if "filter_applied" in query_dict.keys():
|
||||
update_saved_filter_cache(self.request, saved_filters)
|
||||
elif saved_filters.get(self.request.session.session_key):
|
||||
query_dict = saved_filters[self.request.session.session_key][
|
||||
"query_dict"
|
||||
]
|
||||
|
||||
self._saved_filters = query_dict
|
||||
queryset = self.filter_class(query_dict, queryset).qs
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs: Any):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["view_id"] = self.view_id
|
||||
context["columns"] = self.visible_column
|
||||
context["toggle_form"] = self.toggle_form
|
||||
context["search_url"] = self.search_url
|
||||
|
||||
context["action_method"] = self.action_method
|
||||
context["actions"] = self.actions
|
||||
|
||||
context["option_method"] = self.option_method
|
||||
context["options"] = self.options
|
||||
context["row_attrs"] = self.row_attrs
|
||||
|
||||
context["show_filter_tags"] = self.show_filter_tags
|
||||
context["bulk_select_option"] = self.bulk_select_option
|
||||
context["row_status_class"] = self.row_status_class
|
||||
context["sortby_key"] = self.sortby_key
|
||||
context["sortby_mapping"] = self.sortby_mapping
|
||||
context["row_status_indications"] = self.row_status_indications
|
||||
context["saved_filters"] = self._saved_filters
|
||||
|
||||
context["select_all_ids"] = self.select_all
|
||||
if self._saved_filters.get("field"):
|
||||
active_group = models.ActiveGroup.objects.filter(
|
||||
created_by=self.request.user,
|
||||
path=self.request.path,
|
||||
group_by_field=self._saved_filters["field"],
|
||||
).first()
|
||||
if active_group:
|
||||
context["active_target"] = active_group.group_target
|
||||
|
||||
queryset = self.get_queryset()
|
||||
|
||||
if self.show_filter_tags:
|
||||
data_dict = parse_qs(self._saved_filters.urlencode())
|
||||
data_dict = get_key_instances(self.model, data_dict)
|
||||
keys_to_remove = [
|
||||
key
|
||||
for key, value in data_dict.items()
|
||||
if value[0] in ["unknown", "on"] + self.filter_keys_to_remove
|
||||
]
|
||||
|
||||
for key in keys_to_remove:
|
||||
data_dict.pop(key)
|
||||
context["filter_dict"] = data_dict
|
||||
|
||||
request = self.request
|
||||
ordered_ids = list(queryset.values_list("id", flat=True))
|
||||
model = queryset.model
|
||||
is_first_sort = False
|
||||
query_dict = self.request.GET
|
||||
if (
|
||||
not request.GET.get(self.sortby_key)
|
||||
and not self._saved_filters.get(self.sortby_key)
|
||||
) or (
|
||||
not request.GET.get(self.sortby_key)
|
||||
and self._saved_filters.get(self.sortby_key)
|
||||
):
|
||||
is_first_sort = True
|
||||
query_dict = self._saved_filters
|
||||
|
||||
if query_dict.get(self.sortby_key):
|
||||
queryset = sortby(
|
||||
query_dict, queryset, self.sortby_key, is_first_sort=is_first_sort
|
||||
)
|
||||
ordered_ids = [instance.id for instance in queryset]
|
||||
setattr(model, "ordered_ids", ordered_ids)
|
||||
|
||||
context["queryset"] = paginator_qry(
|
||||
queryset, self._saved_filters.get("page"), self.records_per_page
|
||||
)
|
||||
|
||||
if request and self._saved_filters.get("field"):
|
||||
field = self._saved_filters.get("field")
|
||||
self.template_name = "generic/group_by.html"
|
||||
if isinstance(queryset, Page):
|
||||
queryset = self.filter_class(
|
||||
request.GET, queryset=queryset.object_list.model.objects.all()
|
||||
).qs
|
||||
groups = group_by_queryset(
|
||||
queryset, field, self._saved_filters.get("page"), "page"
|
||||
)
|
||||
context["groups"] = paginator_qry(
|
||||
groups, self._saved_filters.get("page"), 10
|
||||
)
|
||||
cache[self.request.session.session_key][HorillaListView] = context
|
||||
from horilla.urls import urlpatterns, path
|
||||
|
||||
self.export_path = f"export-list-view-{get_short_uuid(4)}/"
|
||||
|
||||
urlpatterns.append(path(self.export_path, self.export_data))
|
||||
context["export_path"] = self.export_path
|
||||
return context
|
||||
|
||||
def select_all(self, *args, **kwargs):
|
||||
"""
|
||||
Select all method
|
||||
"""
|
||||
return json.dumps(list(self.get_queryset().values_list("id", flat=True)))
|
||||
|
||||
def export_data(self, *args, **kwargs):
|
||||
"""
|
||||
Export list view visible columns
|
||||
"""
|
||||
from import_export import resources, fields
|
||||
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
ids = eval(request.GET["ids"])
|
||||
queryset = self.model.objects.filter(id__in=ids)
|
||||
|
||||
MODEL = self.model
|
||||
FIELDS = self.visible_column
|
||||
|
||||
class HorillaListViewResorce(resources.ModelResource):
|
||||
id = fields.Field(column_name="ID")
|
||||
|
||||
class Meta:
|
||||
model = MODEL
|
||||
fields = []
|
||||
|
||||
def dehydrate_id(self, instance):
|
||||
return instance.pk
|
||||
|
||||
def before_export(self, queryset, *args, **kwargs):
|
||||
return super().before_export(queryset, *args, **kwargs)
|
||||
|
||||
for field_tuple in FIELDS:
|
||||
dynamic_fn_str = f"def dehydrate_{field_tuple[1]}(self, instance):return str(getattribute(instance, '{field_tuple[1]}'))"
|
||||
exec(dynamic_fn_str)
|
||||
dynamic_fn = locals()[f"dehydrate_{field_tuple[1]}"]
|
||||
locals()[field_tuple[1]] = fields.Field(column_name=field_tuple[0])
|
||||
|
||||
book_resource = HorillaListViewResorce()
|
||||
|
||||
# Export the data using the resource
|
||||
dataset = book_resource.export(queryset)
|
||||
|
||||
excel_data = dataset.export("xls")
|
||||
|
||||
# Set the response headers
|
||||
file_name = self.export_file_name
|
||||
if not file_name:
|
||||
file_name = "quick_export"
|
||||
response = HttpResponse(excel_data, content_type="application/vnd.ms-excel")
|
||||
response["Content-Disposition"] = f'attachment; filename="{file_name}.xls"'
|
||||
return response
|
||||
|
||||
|
||||
class HorillaSectionView(TemplateView):
|
||||
"""
|
||||
Horilla Template View
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaListView)
|
||||
|
||||
nav_url: str = ""
|
||||
view_url: str = ""
|
||||
|
||||
# view container id is used to wrap the component view with th id
|
||||
view_container_id: str = ""
|
||||
|
||||
script_static_paths: list = []
|
||||
style_static_paths: list = []
|
||||
|
||||
template_name = "generic/horilla_section.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["nav_url"] = self.nav_url
|
||||
context["view_url"] = self.view_url
|
||||
context["view_container_id"] = self.view_container_id
|
||||
context["script_static_paths"] = self.script_static_paths
|
||||
context["style_static_paths"] = self.style_static_paths
|
||||
return context
|
||||
|
||||
|
||||
class HorillaDetailedView(DetailView):
|
||||
"""
|
||||
HorillDetailedView
|
||||
"""
|
||||
|
||||
title = "Detailed View"
|
||||
template_name = "generic/horilla_detailed_view.html"
|
||||
header: dict = {"title": "Horilla", "subtitle": "Horilla Detailed View"}
|
||||
body: list = []
|
||||
|
||||
action_method: list = []
|
||||
actions: list = []
|
||||
|
||||
ids_key: str = "instance_ids"
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaDetailedView)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs: Any):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
instance_ids = eval(str(self.request.GET.get(self.ids_key)))
|
||||
|
||||
pk = context["object"].pk
|
||||
|
||||
url = resolve(self.request.path)
|
||||
key = list(url.kwargs.keys())[0]
|
||||
|
||||
url_name = url.url_name
|
||||
|
||||
previous_id, next_id = closest_numbers(instance_ids, pk)
|
||||
|
||||
next_url = reverse(url_name, kwargs={key: next_id})
|
||||
previous_url = reverse(url_name, kwargs={key: previous_id})
|
||||
if instance_ids:
|
||||
context["instance_ids"] = str(instance_ids)
|
||||
context["ids_key"] = self.ids_key
|
||||
|
||||
context["next_url"] = next_url
|
||||
context["previous_url"] = previous_url
|
||||
|
||||
context["title"] = self.title
|
||||
context["header"] = self.header
|
||||
context["body"] = self.body
|
||||
context["actions"] = self.actions
|
||||
context["action_method"] = self.action_method
|
||||
|
||||
cache[self.request.session.session_key][HorillaDetailedView] = context
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class HorillaTabView(TemplateView):
|
||||
"""
|
||||
HorillaTabView
|
||||
"""
|
||||
|
||||
template_name = "generic/horilla_tabs.html"
|
||||
|
||||
tabs: list = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaTabView)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.user:
|
||||
active_tab = models.ActiveTab.objects.filter(
|
||||
created_by=self.request.user, path=self.request.path
|
||||
).first()
|
||||
if active_tab:
|
||||
context["active_target"] = active_tab.tab_target
|
||||
context["tabs"] = self.tabs
|
||||
|
||||
cache[self.request.session.session_key][HorillaTabView] = context
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class HorillaCardView(ListView):
|
||||
"""
|
||||
HorillaCardView
|
||||
"""
|
||||
|
||||
filter_class: FilterSet = None
|
||||
|
||||
view_id: str = get_short_uuid(4, prefix="hcv")
|
||||
|
||||
template_name = "generic/horilla_card.html"
|
||||
context_object_name = "queryset"
|
||||
|
||||
search_url: str = ""
|
||||
|
||||
details: dict = {}
|
||||
|
||||
actions: list = []
|
||||
|
||||
card_attrs: str = """"""
|
||||
|
||||
show_filter_tags: bool = True
|
||||
filter_keys_to_remove: list = []
|
||||
|
||||
records_per_page: int = 50
|
||||
card_status_class: str = """"""
|
||||
card_status_indications: list = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaCardView)
|
||||
self._saved_filters = QueryDict()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.filter_class:
|
||||
query_dict = self.request.GET
|
||||
if "filter_applied" in query_dict.keys():
|
||||
update_saved_filter_cache(self.request, saved_filters)
|
||||
elif saved_filters.get(self.request.session.session_key):
|
||||
query_dict = saved_filters[self.request.session.session_key][
|
||||
"query_dict"
|
||||
]
|
||||
|
||||
self._saved_filters = query_dict
|
||||
queryset = self.filter_class(query_dict, queryset).qs
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
queryset = self.get_queryset()
|
||||
|
||||
context["view_id"] = self.view_id
|
||||
context["search_url"] = self.search_url
|
||||
context["card_attrs"] = self.card_attrs
|
||||
context["actions"] = self.actions
|
||||
context["details"] = self.details
|
||||
context["show_filter_tags"] = self.show_filter_tags
|
||||
context["card_status_class"] = self.card_status_class
|
||||
context["card_status_indications"] = self.card_status_indications
|
||||
|
||||
context["queryset"] = paginator_qry(
|
||||
queryset, self.request.GET.get("page"), self.records_per_page
|
||||
)
|
||||
|
||||
if self.show_filter_tags:
|
||||
data_dict = parse_qs(self._saved_filters.urlencode())
|
||||
data_dict = get_key_instances(self.model, data_dict)
|
||||
keys_to_remove = [
|
||||
key
|
||||
for key, value in data_dict.items()
|
||||
if value[0] in ["unknown", "on"] + self.filter_keys_to_remove
|
||||
]
|
||||
|
||||
for key in keys_to_remove:
|
||||
data_dict.pop(key)
|
||||
context["filter_dict"] = data_dict
|
||||
|
||||
cache[self.request.session.session_key][HorillaCardView] = context
|
||||
return context
|
||||
|
||||
|
||||
class ReloadMessages(TemplateView):
|
||||
template_name = "generic/messages.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
return context
|
||||
|
||||
|
||||
dynamic_create_cache = {}
|
||||
|
||||
|
||||
def save(self: forms.ModelForm, commit=True, *args, **kwargs):
|
||||
"""
|
||||
This method is used to super save the form using custom logic
|
||||
"""
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
|
||||
dynamic_field = request.GET["dynamic_field"]
|
||||
response = None
|
||||
if commit:
|
||||
response = super(type(self), self).save(*args, **kwargs)
|
||||
new_isntance_pk = self.instance.pk
|
||||
dynamic_create_cache[request.session.session_key + dynamic_field] = {
|
||||
"dynamic_field": dynamic_field,
|
||||
"value": new_isntance_pk,
|
||||
"form": self,
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
|
||||
class HorillaFormView(FormView):
|
||||
"""
|
||||
HorillaFormView
|
||||
"""
|
||||
|
||||
class HttpResponse:
|
||||
def __new__(
|
||||
self, content: str = "", targets_to_reload: list = [], script: str = ""
|
||||
) -> HttpResponse:
|
||||
targets_to_reload = list(set(targets_to_reload))
|
||||
targets_to_reload.append("#reloadMessagesButton")
|
||||
script_id = get_short_uuid(4)
|
||||
script = (
|
||||
f"<script id='scriptTarget{script_id}'>"
|
||||
+ "{}".format(
|
||||
"".join([f"$(`{target}`).click();" for target in targets_to_reload])
|
||||
)
|
||||
+ f"$('#scriptTarget{script_id}').closest('.oh-modal--show').first().removeClass('oh-modal--show');"
|
||||
+ "$('.reload-record').click();"
|
||||
+ "$('.reload-field').click();"
|
||||
+ script
|
||||
+ "</script>"
|
||||
)
|
||||
|
||||
reload_response = HttpResponse(script).content
|
||||
|
||||
user_response = HttpResponse(content).content
|
||||
response = HttpResponse(reload_response + user_response)
|
||||
self = response
|
||||
return response
|
||||
|
||||
model: object = None
|
||||
view_id: str = get_short_uuid(4)
|
||||
form_class: forms.ModelForm = None
|
||||
template_name = "generic/horilla_form.html"
|
||||
form_disaply_attr: str = ""
|
||||
new_display_title: str = "Add New"
|
||||
close_button_attrs: str = """"""
|
||||
submit_button_attrs: str = """"""
|
||||
|
||||
# NOTE: Dynamic create view's forms save method will be overwritten
|
||||
is_dynamic_create_view: bool = False
|
||||
dynamic_create_fields: list = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaFormView)
|
||||
|
||||
if self.form_class:
|
||||
setattr(self.form_class, "structured", structured)
|
||||
|
||||
def get(
|
||||
self, request: HttpRequest, pk=None, *args: str, **kwargs: Any
|
||||
) -> HttpResponse:
|
||||
response = super().get(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
def post(
|
||||
self, request: HttpRequest, pk=None, *args: str, **kwargs: Any
|
||||
) -> HttpResponse:
|
||||
self.get_form()
|
||||
response = super().post(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["dynamic_create_fields"] = self.dynamic_create_fields
|
||||
context["form_class_path"] = self.form_class_path
|
||||
context["view_id"] = self.view_id
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
pk = self.kwargs.get("pk")
|
||||
instance = self.model.objects.filter(pk=pk).first()
|
||||
|
||||
data = None
|
||||
files = None
|
||||
if self.request.method == "POST":
|
||||
data = self.request.POST
|
||||
files = self.request.FILES
|
||||
form = self.form_class(data, files, instance=instance)
|
||||
|
||||
if self.is_dynamic_create_view:
|
||||
setattr(type(form), "save", save)
|
||||
|
||||
self.form_class_path = form.__class__.__module__ + "." + form.__class__.__name__
|
||||
if self.request.method == "GET":
|
||||
for dynamic_tuple in self.dynamic_create_fields:
|
||||
view = dynamic_tuple[1]
|
||||
view.display_title = "Dynamic create"
|
||||
|
||||
field = dynamic_tuple[0]
|
||||
dynamic_create_cache[self.request.session.session_key + field] = {
|
||||
"dynamic_field": field,
|
||||
"value": getattr(form.instance, field, ""),
|
||||
"form": form,
|
||||
}
|
||||
|
||||
from horilla.urls import urlpatterns, path
|
||||
|
||||
urlpatterns.append(
|
||||
path(
|
||||
f"dynamic-path-{field}-{self.request.session.session_key}",
|
||||
view.as_view(),
|
||||
name=f"dynamic-path-{field}-{self.request.session.session_key}",
|
||||
)
|
||||
)
|
||||
queryset = form.fields[field].queryset
|
||||
choices = [(instance.id, instance) for instance in queryset]
|
||||
choices.insert(0, ("", "Select option"))
|
||||
choices.append(("dynamic_create", "Dynamic create"))
|
||||
form.fields[field] = forms.ChoiceField(
|
||||
choices=choices,
|
||||
label=form.fields[field].label,
|
||||
required=form.fields[field].required,
|
||||
widget=forms.Select(attrs=form.fields[field].widget.attrs),
|
||||
)
|
||||
if pk:
|
||||
form.instance = instance
|
||||
title = str(instance)
|
||||
if self.form_disaply_attr:
|
||||
title = getattribute(instance, self.form_disaply_attr)
|
||||
if instance:
|
||||
self.form_class.verbose_name = title
|
||||
else:
|
||||
self.form_class.verbose_name = self.new_display_title
|
||||
form.close_button_attrs = self.close_button_attrs
|
||||
form.submit_button_attrs = self.submit_button_attrs
|
||||
cache[self.request.session.session_key][HorillaFormView] = form
|
||||
self.form = form
|
||||
return form
|
||||
|
||||
|
||||
class HorillaNavView(TemplateView):
|
||||
"""
|
||||
HorillaNavView
|
||||
|
||||
filter form submit button id: applyFilter
|
||||
"""
|
||||
|
||||
template_name = "generic/horilla_nav.html"
|
||||
|
||||
nav_title: str = ""
|
||||
search_url: str = ""
|
||||
search_swap_target: str = ""
|
||||
search_input_attrs: str = ""
|
||||
search_in: list = []
|
||||
actions: list = []
|
||||
group_by_fields: list = []
|
||||
filter_form_context_name: str = ""
|
||||
filter_instance: FilterSet = None
|
||||
filter_instance_context_name: str = ""
|
||||
filter_body_template: str = ""
|
||||
view_types: list = []
|
||||
create_attrs: str = """"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
request = getattr(_thread_locals, "request", None)
|
||||
self.request = request
|
||||
update_initial_cache(request, cache, HorillaNavView)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["nav_title"] = self.nav_title
|
||||
context["search_url"] = self.search_url
|
||||
context["search_swap_target"] = self.search_swap_target
|
||||
context["search_input_attrs"] = self.search_input_attrs
|
||||
context["group_by_fields"] = self.group_by_fields
|
||||
context["actions"] = self.actions
|
||||
context["filter_body_template"] = self.filter_body_template
|
||||
context["view_types"] = self.view_types
|
||||
context["create_attrs"] = self.create_attrs
|
||||
context["search_in"] = self.search_in
|
||||
context["filter_instance_context_name"] = self.filter_instance
|
||||
if self.filter_instance:
|
||||
context[self.filter_form_context_name] = self.filter_instance.form
|
||||
cache[self.request.session.session_key][HorillaNavView] = context
|
||||
return context
|
||||
0
horilla_views/migrations/__init__.py
Normal file
0
horilla_views/migrations/__init__.py
Normal file
95
horilla_views/models.py
Normal file
95
horilla_views/models.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from horilla.models import HorillaModel
|
||||
from base.thread_local_middleware import _thread_locals
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class ToggleColumn(HorillaModel):
|
||||
"""
|
||||
ToggleColumn
|
||||
"""
|
||||
|
||||
user_id = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="user_excluded_column",
|
||||
editable=False,
|
||||
)
|
||||
path = models.CharField(max_length=256)
|
||||
excluded_columns = models.JSONField(default=list)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
request = getattr(_thread_locals, "request", {})
|
||||
user = request.user
|
||||
self.user_id = user
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.user_id.employee_get)
|
||||
|
||||
|
||||
class ActiveTab(HorillaModel):
|
||||
"""
|
||||
ActiveTab
|
||||
"""
|
||||
|
||||
path = models.CharField(max_length=256)
|
||||
tab_target = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class ActiveGroup(HorillaModel):
|
||||
"""
|
||||
ActiveGroup
|
||||
"""
|
||||
path = models.CharField(max_length=256)
|
||||
group_target = models.CharField(max_length=256)
|
||||
group_by_field = models.CharField(max_length=256)
|
||||
|
||||
|
||||
class ParentModel(models.Model):
|
||||
""" """
|
||||
|
||||
title = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
|
||||
|
||||
class DemoCompany(models.Model):
|
||||
title = models.CharField(max_length=20)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
|
||||
|
||||
class DemoDepartment(models.Model):
|
||||
"""
|
||||
DemoDepartment
|
||||
"""
|
||||
|
||||
title = models.CharField(max_length=20)
|
||||
company_id = models.ForeignKey(
|
||||
DemoCompany,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Company",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.title
|
||||
|
||||
|
||||
class childModel(models.Model):
|
||||
""" """
|
||||
|
||||
title_id = models.ForeignKey(
|
||||
ParentModel, on_delete=models.CASCADE, verbose_name="Title"
|
||||
)
|
||||
department_id = models.ForeignKey(
|
||||
DemoDepartment, on_delete=models.CASCADE, null=True, verbose_name="Department"
|
||||
)
|
||||
description = models.TextField()
|
||||
0
horilla_views/templates/cbv/filter_form.html
Normal file
0
horilla_views/templates/cbv/filter_form.html
Normal file
32
horilla_views/templates/generic/as_list.html
Normal file
32
horilla_views/templates/generic/as_list.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% load i18n %}
|
||||
<form hx-get="{% url "toggle-columns" %}" hx-swap="none">
|
||||
<input type="hidden" name="path" value="{{request.path_info}}">
|
||||
<ul class="oh-dropdown__items">
|
||||
<div class="oh-dropdown_btn-header">
|
||||
<button class="oh-btn oh-btn--success-outline">
|
||||
{% trans "Select All Records" %}
|
||||
</button>
|
||||
<button class="oh-btn oh-btn--primary-outline">
|
||||
{% trans "Unselect All Records" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% for field in form.visible_fields %}
|
||||
<li class="oh-dropdown__item oh-sticy-dropdown-item">
|
||||
<span>{{field.label}}</span>
|
||||
<span class="oh-table__checkbox">
|
||||
<input type="hidden" name="{{field.name}}" onchange="$(this).closest('form').find('[type=submit]').click();" {% if not field.initial %} value ="false" {% endif %}>
|
||||
<input type="checkbox" id="toggle_{{field.name}}" {% if field.initial %} checked {% endif %} onclick="
|
||||
value='';
|
||||
if (!$(this).is(':checked')) {
|
||||
value = 'off'
|
||||
}
|
||||
$(this).siblings('input[type=hidden]').val(value).change();
|
||||
"
|
||||
>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="submit" hidden>
|
||||
</form>
|
||||
54
horilla_views/templates/generic/components.html
Normal file
54
horilla_views/templates/generic/components.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<div
|
||||
class="oh-modal"
|
||||
id="genericModal"
|
||||
role="dialog"
|
||||
aria-labelledby="genericModal"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="oh-modal__dialog" id="genericModalBody"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).on("htmx:afterOnLoad", function (event) {
|
||||
$("[data-toggle='oh-modal-toggle']").click(function (e) {
|
||||
e.preventDefault();
|
||||
let clickedEl = $(e.target).closest('[data-toggle = "oh-modal-toggle"]');
|
||||
if (clickedEl != null) {
|
||||
const targetEl = clickedEl.data("target");
|
||||
$(targetEl).addClass("oh-modal--show");
|
||||
}
|
||||
});
|
||||
});
|
||||
function switchTab(e) {
|
||||
let parentContainerEl = e.target.closest(".oh-tabs");
|
||||
let tabElement = e.target.closest(".oh-tabs__tab");
|
||||
|
||||
let targetSelector = e.target.dataset.target;
|
||||
let targetEl = parentContainerEl
|
||||
? parentContainerEl.querySelector(targetSelector)
|
||||
: null;
|
||||
|
||||
// Highlight active tabs
|
||||
if (tabElement && !tabElement.classList.contains("oh-tabs__tab--active")) {
|
||||
parentContainerEl
|
||||
.querySelectorAll(".oh-tabs__tab--active")
|
||||
.forEach(function (item) {
|
||||
item.classList.remove("oh-tabs__tab--active");
|
||||
});
|
||||
|
||||
if (!tabElement.classList.contains("oh-tabs__new-tab")) {
|
||||
tabElement.classList.add("oh-tabs__tab--active");
|
||||
}
|
||||
}
|
||||
|
||||
// Switch tabs
|
||||
if (targetEl && !targetEl.classList.contains("oh-tabs__content--active")) {
|
||||
parentContainerEl
|
||||
.querySelectorAll(".oh-tabs__content--active")
|
||||
.forEach(function (item) {
|
||||
item.classList.remove("oh-tabs__content--active");
|
||||
});
|
||||
targetEl.classList.add("oh-tabs__content--active");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
9
horilla_views/templates/generic/custom_list.html
Normal file
9
horilla_views/templates/generic/custom_list.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="oh-wrapper" id="{{view_id|safe}}">
|
||||
<h2>
|
||||
Header
|
||||
</h2>
|
||||
{% include "generic/horilla_list.html" %}
|
||||
<h2>
|
||||
Footer
|
||||
</h2>
|
||||
</div>
|
||||
80
horilla_views/templates/generic/filter_tags.html
Normal file
80
horilla_views/templates/generic/filter_tags.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% load i18n %} {% load basefilters %}
|
||||
|
||||
<!-- filter items showing here -->
|
||||
<div style="display: none">{{filter_dict}}</div>
|
||||
<script>
|
||||
function fieldLabel(value, field) {
|
||||
fiedlElem = $(`#applyFilter`).closest(`form`).find(`[name=${field}]`);
|
||||
if (fiedlElem.is("select")) {
|
||||
// my conditions
|
||||
if (fiedlElem.is("select[multiple]")) {
|
||||
values = fiedlElem.val();
|
||||
values.push(field);
|
||||
$.each(values, function (index, value) {
|
||||
fiedlElem.append(
|
||||
$("<option>", {
|
||||
value: value,
|
||||
text: fiedlElem.find(`[value=${field}]`).html(),
|
||||
})
|
||||
);
|
||||
});
|
||||
} else {
|
||||
fiedlElem.val(value)
|
||||
if (!fiedlElem == "field") {
|
||||
fiedlElem.change();
|
||||
}else{
|
||||
fiedlElem.select2('destroy');
|
||||
fiedlElem.select2()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!fiedlElem.is(":focus")) {
|
||||
fiedlElem.val(value);
|
||||
}
|
||||
}
|
||||
if (field == "field") {
|
||||
return $(`option[value="${value}"]`).html();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
$(document).ready(function () {
|
||||
var nav = $("#filterTagContainerSectionNav");
|
||||
tags = $(`
|
||||
{% if filter_dict %}
|
||||
<span class="oh-titlebar__tag oh-titlebar__tag--custom"
|
||||
>{% trans 'Filters' %}:</span
|
||||
>
|
||||
{% endif %}
|
||||
{%for field,values in filter_dict.items %} {% if values %}
|
||||
{% with translation_field=field|filter_field %}
|
||||
<span class="oh-titlebar__tag filter-field" >
|
||||
{% trans translation_field %} :
|
||||
{% for value in values %}
|
||||
${fieldLabel("{% trans value %}", "{{field}}")}
|
||||
{% endfor %}
|
||||
<button class="oh-titlebar__tag-close" onclick="clearFilterFromTag($(this))" data-x-field="{{field}}">
|
||||
<ion-icon
|
||||
name="close-outline"
|
||||
role="img"
|
||||
class="md hydrated close-icon"
|
||||
aria-label="close outline"
|
||||
>
|
||||
</ion-icon>
|
||||
</button>
|
||||
</span>
|
||||
{% endwith %} {% endif %} {% endfor %}
|
||||
{% if filter_dict %}
|
||||
<span class="oh-titlebar__tag oh-titlebar__tag--custom" title="{% trans 'Clear All' %}" role="button" onclick="clearAllFilter($(this));"
|
||||
><ion-icon class="close-icon" name="close-outline"></ion-icon></span
|
||||
>
|
||||
{% endif %}
|
||||
`);
|
||||
|
||||
nav.html(tags);
|
||||
$("oh-tabs__tab oh-tabs__tab--active:first").click();
|
||||
});
|
||||
</script>
|
||||
<div
|
||||
id="filterTagContainerSectionNav"
|
||||
class="oh-titlebar-container__filters mb-2 mt-0 oh-wrapper"
|
||||
></div>
|
||||
61
horilla_views/templates/generic/form.html
Normal file
61
horilla_views/templates/generic/form.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% load widget_tweaks %} {% load i18n %}
|
||||
<style>
|
||||
.condition-highlight {
|
||||
background-color: #ffa5000f;
|
||||
}
|
||||
</style>
|
||||
{% if form.verbose_name %}
|
||||
<div class="oh-modal__dialog-header">
|
||||
<h2 class="oh-modal__dialog-title" id="createTitle">
|
||||
{{form.verbose_name}}
|
||||
</h2>
|
||||
<button type="button" class="oh-modal__close--custom" onclick="$(this).closest('.oh-modal--show').removeClass('oh-modal--show')" aria-label="Close" {{form.close_button_attrs|safe}}>
|
||||
<ion-icon name="close-outline" role="img" class="md hydrated" aria-label="close outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="oh-modal__dialog-body">
|
||||
<div class="oh-general__tab-target oh-profile-section" id="personal">
|
||||
<div class="oh-profile-section__card row">
|
||||
<div class="row">
|
||||
<div class="col-12">{{ form.non_field_errors }}</div>
|
||||
{% for field in form.visible_fields %}
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="oh-label__info" for="id_{{ field.name }}">
|
||||
<label class="oh-label" for="id_{{ field.name }}"
|
||||
>{% trans field.label %}</label
|
||||
>
|
||||
{% if field.help_text != '' %}
|
||||
<span
|
||||
class="oh-info mr-2"
|
||||
title="{{ field.help_text|safe }}"
|
||||
></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="oh-switch" style="width: 30px">
|
||||
{{ field|add_class:'oh-switch__checkbox' }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="dynamic_field_{{field.name}}">
|
||||
{{ field|add_class:'form-control' }} {% endif %} {{ field.errors }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for field in form.hidden_fields %} {{ field }} {% endfor %}
|
||||
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<button
|
||||
type="submit"
|
||||
class="oh-btn oh-btn--secondary mt-2 mr-0 pl-4 pr-5 oh-btn--w-100-resp"
|
||||
{{form.submit_button_attrs|safe}}
|
||||
>
|
||||
{% trans 'Save' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
415
horilla_views/templates/generic/group_by.html
Normal file
415
horilla_views/templates/generic/group_by.html
Normal file
@@ -0,0 +1,415 @@
|
||||
{% load static i18n generic_template_filters %}
|
||||
<div id="{{view_id|safe}}">
|
||||
{% if bulk_select_option %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<div class="oh-checkpoint-badge text-success"
|
||||
onclick="
|
||||
addToSelectedId({{select_all_ids|safe}});
|
||||
selectSelected('#{{view_id|safe}}');
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
"
|
||||
style="cursor: pointer;">
|
||||
{% trans "Select All" %}{{select_all_path}}
|
||||
</div>
|
||||
<div
|
||||
id="unselect_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-secondary d-none"
|
||||
style="cursor: pointer;"
|
||||
onclick="
|
||||
$('#selectedInstances').attr('data-ids',JSON.stringify([]));
|
||||
selectSelected('#{{view_id|safe}}');
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
$('#{{view_id}} .list-table-row').prop('checked',false);
|
||||
$('#{{view_id}} .highlight-selected').removeClass('highlight-selected');
|
||||
$('#{{view_id}} .bulk-list-table-row').prop('checked',false);
|
||||
"
|
||||
>
|
||||
{% trans "Unselect All" %}
|
||||
</div>
|
||||
<div class="oh-checkpoint-badge text-danger d-none"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
<span id="count_{{view_id}}">
|
||||
0
|
||||
</span> {% trans "Selected" %}
|
||||
</div>
|
||||
<div
|
||||
id="export_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-info d-none"
|
||||
style="cursor: pointer;"
|
||||
onclick="
|
||||
selectedIds = $('#selectedInstances').attr('data-ids')
|
||||
window.location.href = '{{export_path}}' + '?ids='+selectedIds
|
||||
"
|
||||
>
|
||||
{% trans "Export" %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<button class="reload-record" hidden hx-get="{{request.path}}?{{request.GET.urlencode}}" hx-target="#{{view_id|safe}}" hx-swap="outerHTML">
|
||||
</button>
|
||||
{% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %}
|
||||
<div class="oh-card">
|
||||
{% for group in groups %}
|
||||
<div class="oh-accordion-meta">
|
||||
<div class="oh-accordion-meta__item">
|
||||
<div class="oh-accordion-meta__header"
|
||||
data-field="{{saved_filters.field}}"
|
||||
data-path="{{request.path}}"
|
||||
data-group="{{forloop.counter}}"
|
||||
data-open=false
|
||||
onclick="
|
||||
event.stopPropagation()
|
||||
$(this).parent().find('.oh-accordion-meta__body').toggleClass('d-none')
|
||||
$(this).toggleClass('oh-accordion-meta__header--show')
|
||||
"
|
||||
>
|
||||
<span class="oh-accordion-meta__title p-2">
|
||||
<span class="oh-accordion-meta__title pt-3 pb-3">
|
||||
<div class="oh-tabs__input-badge-container">
|
||||
<span
|
||||
class="oh-badge oh-badge--secondary oh-badge--small oh-badge--round mr-1"
|
||||
title="5 Candidates"
|
||||
>
|
||||
{{group.list.paginator.count}}
|
||||
</span>
|
||||
{{group.grouper|capfirst}}
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="oh-accordion-meta__body d-none">
|
||||
<div class="oh-sticky-table oh-sticky-table--no-overflow mb-5" style="
|
||||
width: 100%;
|
||||
width: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
width: fill-available;
|
||||
">
|
||||
<div class="oh-sticky-table__table">
|
||||
<div class="oh-sticky-table__thead">
|
||||
<div class="oh-sticky-table__tr">
|
||||
{% if bulk_select_option %}
|
||||
<div
|
||||
class="oh-sticky-table__th"
|
||||
style="width: 10px; z-index: 12 !important"
|
||||
>
|
||||
<div class="centered-div" align="center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="oh-input oh-input__checkbox bulk-list-table-row"
|
||||
onchange="
|
||||
$(this).closest('.oh-sticky-table').find('.list-table-row').prop('checked',$(this).is(':checked')).change();
|
||||
$(document).ready(function () {
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
});
|
||||
"
|
||||
title="Select All"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for cell in columns %}
|
||||
<div class="oh-sticky-table__th"
|
||||
|
||||
>{{cell.0}}</div>
|
||||
{% endfor %}{% if options or option_method%}
|
||||
<div class="oh-sticky-table__th" >
|
||||
<div style="width: 200px;">
|
||||
{% trans "Options" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% if actions or action_method %}
|
||||
<div class="oh-sticky-table__th oh-sticky-table__right" style="z-index:12 !important;">
|
||||
<div style="width: 200px;">
|
||||
{% trans "Actions" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table__tbody">
|
||||
{% for instance in group.list %}
|
||||
<div
|
||||
class="oh-sticky-table__tr"
|
||||
draggable="true"
|
||||
data-instance-id="{{instance.id}}"
|
||||
{{row_attrs|format:instance|safe}}
|
||||
>
|
||||
{% if bulk_select_option %}
|
||||
<div
|
||||
class="oh-sticky-table__sd {{row_status_class|format:instance|safe}}"
|
||||
onclick="event.stopPropagation()"
|
||||
style="width: 10px; z-index: 11 !important"
|
||||
>
|
||||
<div class="centered-div" align="center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="oh-input oh-input__checkbox list-table-row"
|
||||
onchange="
|
||||
highlightRow($(this))
|
||||
$(document).ready(function () {
|
||||
if (!element.is(':checked')) {
|
||||
removeId(element)
|
||||
}
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
});
|
||||
"
|
||||
value = "{{instance.pk}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for cell in columns %}
|
||||
{% with attribute=cell.1 index=forloop.counter %} {% if not cell.2 %}
|
||||
<div
|
||||
class="{% if index == 1 %} oh-sticky-table__sd {% else %} oh-sticky-table__td{% endif %}"
|
||||
>
|
||||
{{instance|getattribute:attribute|safe}}
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="{% if index == 1 %} oh-sticky-table__sd {% else %} oh-sticky-table__td{% endif %}"
|
||||
>
|
||||
<div class="oh-profile oh-profile--md">
|
||||
<div class="oh-profile__avatar mr-1">
|
||||
<img
|
||||
src="{{instance|getattribute:cell.2}}"
|
||||
class="oh-profile__image"
|
||||
/>
|
||||
</div>
|
||||
<span class="oh-profile__name oh-text--dark">
|
||||
{{instance|getattribute:attribute}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endwith %} {% endfor %} {% if options or option_method %}
|
||||
<div class="oh-sticky-table__td oh-permission-table--toggle">
|
||||
{% if not option_method %}
|
||||
<div class="oh-btn-group">
|
||||
{% for option in options %}
|
||||
<a
|
||||
href="#"
|
||||
title="{{option.option|safe}}"
|
||||
{{option.attrs|format:instance|safe}}
|
||||
>
|
||||
<ion-icon name="{{option.icon}}"></ion-icon>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %} {{instance|getattribute:option_method|safe}} {% endif %}
|
||||
</div>
|
||||
{% endif %} {% if actions or action_method %}
|
||||
<div class="oh-sticky-table__td oh-sticky-table__right">
|
||||
{% if not action_method %}
|
||||
<div class="oh-btn-group">
|
||||
{% for action in actions %}
|
||||
<a
|
||||
href="#"
|
||||
title="{{action.action|safe}}"
|
||||
{{action.attrs|format:instance|safe}}
|
||||
>
|
||||
<ion-icon name="{{action.icon}}"></ion-icon>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %} {{instance|getattribute:action_method|safe}} {% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-pagination">
|
||||
<span class="oh-pagination__page">
|
||||
{% trans "Page" %} {{ group.list.number }}
|
||||
{% trans "of" %} {{ group.list.paginator.num_pages }}.
|
||||
</span>
|
||||
<nav class="oh-pagination__nav">
|
||||
<div class="oh-pagination__input-container me-3">
|
||||
<span class="oh-pagination__label me-1"
|
||||
>{% trans "Page" %}</span
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="{{group.dynamic_name}}"
|
||||
class="oh-pagination__input"
|
||||
value="{{group.list.number}}"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}"
|
||||
hx-target="#{{view_id}}"
|
||||
min="1"
|
||||
/>
|
||||
<span class="oh-pagination__label"
|
||||
>{% trans "of" %}
|
||||
{{group.list.paginator.num_pages}}</span
|
||||
>
|
||||
</div>
|
||||
<ul class="oh-pagination__items">
|
||||
{% if group.list.has_previous %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-target="#{{view_id}}"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&{{group.dynamic_name}}=1"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "First" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-target="#{{view_id}}"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&{{group.dynamic_name}}={{ group.list.previous_page_number }}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Previous" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %} {% if group.list.has_next %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-target="#{{view_id}}"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&{{group.dynamic_name}}={{ group.list.next_page_number }}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Next" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-target="#{{view_id}}"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&{{group.dynamic_name}}={{ group.list.paginator.num_pages }}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Last" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if groups.paginator.count %}
|
||||
<div class="oh-pagination">
|
||||
<span
|
||||
class="oh-pagination__page"
|
||||
data-toggle="modal"
|
||||
data-target="#addEmployeeModal"
|
||||
>{% trans "Page" %} {{groups.number}} {% trans "of" %} {{groups.paginator.num_pages}}</span
|
||||
>
|
||||
|
||||
<nav class="oh-pagination__nav">
|
||||
<div class="oh-pagination__input-container me-3">
|
||||
<span class="oh-pagination__label me-1">{% trans "Page" %}</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="oh-pagination__input"
|
||||
value="{{groups.number}}"
|
||||
min="1"
|
||||
name="page"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
/>
|
||||
<span class="oh-pagination__label">{% trans "of" %} {{groups.paginator.num_pages}}</span>
|
||||
</div>
|
||||
|
||||
<ul class="oh-pagination__items">
|
||||
{% if groups.has_previous %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-get="{{search_url}}?{{request.GET.urlencode}}&page=1" hx-swap="outerHTML" hx-target="#{{view_id|safe}}" class="oh-pagination__link">{% trans "First" %}</a>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ groups.previous_page_number }}" hx-swap="outerHTML" hx-target="#{{view_id|safe}}" class="oh-pagination__link">{% trans "Previous" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if groups.has_next %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ groups.next_page_number }}" hx-swap="outerHTML" hx-target="#{{view_id|safe}}" class="oh-pagination__link">{% trans "Next" %}</a>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ groups.paginator.num_pages }}" hx-swap="outerHTML" hx-target="#{{view_id|safe}}" class="oh-pagination__link">{% trans "Last" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<script>
|
||||
var tabId = $("#{{view_id}}").closest(".oh-tabs__content").attr("id")
|
||||
let badge = $(`#badge-${tabId}`)
|
||||
let count = "{{queryset.paginator.count}}"
|
||||
let label = badge.attr("data-badge-label") || ""
|
||||
let title = count + " " + label
|
||||
badge.html(count)
|
||||
badge.attr("title",title)
|
||||
</script>
|
||||
{% if bulk_select_option %}
|
||||
<script>
|
||||
ids = JSON.parse(
|
||||
$("#selectedInstances").attr("data-ids") || "[]"
|
||||
);
|
||||
$.each(ids, function (indexInArray, valueOfElement) {
|
||||
$(`#{{view_id|safe}} .oh-sticky-table__tbody .list-table-row[value=${valueOfElement}]`).prop("checked",true).change()
|
||||
});
|
||||
$("#{{view_id|safe}} .oh-sticky-table__tbody .list-table-row").change(function (
|
||||
e
|
||||
) {
|
||||
id = $(this).val()
|
||||
ids = JSON.parse(
|
||||
$("#selectedInstances").attr("data-ids") || "[]"
|
||||
);
|
||||
ids = Array.from(new Set(ids));
|
||||
let index = ids.indexOf(id);
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id);
|
||||
} else {
|
||||
if (!$(this).is(":checked")) {
|
||||
ids.splice(index, 1);
|
||||
}
|
||||
}
|
||||
$("#selectedInstances").attr("data-ids", JSON.stringify(ids));
|
||||
}
|
||||
);
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
$(".oh-accordion-meta__header").click(function (e) {
|
||||
var open = $(this).attr("data-open");
|
||||
open = JSON.parse(open)
|
||||
$(this).attr("data-open", !open);
|
||||
var field = $(this).attr("data-field");
|
||||
var groupIndex = $(this).attr("data-group");
|
||||
var target = `[data-group="${groupIndex}"][data-field="${field}"][data-path="{{request.path}}"][data-open="${open}"]`;
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: "get",
|
||||
url: "{% url 'cbv-active-group' %}",
|
||||
data: {
|
||||
"path":"{{request.path}}",
|
||||
"target":target,
|
||||
"field":field,
|
||||
},
|
||||
success: function (response) {
|
||||
}
|
||||
});
|
||||
});
|
||||
{% if active_target %}
|
||||
$("#{{view_id|safe}}").find(`{{active_target|safe}}`).click();
|
||||
{% endif %}
|
||||
</script>
|
||||
</div>
|
||||
3
horilla_views/templates/generic/hello.html
Normal file
3
horilla_views/templates/generic/hello.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<button>
|
||||
Hello
|
||||
</button>
|
||||
156
horilla_views/templates/generic/horilla_card.html
Normal file
156
horilla_views/templates/generic/horilla_card.html
Normal file
@@ -0,0 +1,156 @@
|
||||
{% load static i18n generic_template_filters %}
|
||||
<div id="{{view_id|safe}}">
|
||||
{% if queryset|length %}
|
||||
{% 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 %}
|
||||
<button class="reload-record" hidden hx-get="{{request.path}}?{{request.GET.urlencode}}" hx-target="#{{view_id|safe}}" hx-swap="outerHTML">
|
||||
</button>
|
||||
{% if show_filter_tags %}
|
||||
{% include "generic/filter_tags.html" %}
|
||||
{% endif %}
|
||||
<div class="oh-layout--grid-3">
|
||||
{% for instance in queryset %}
|
||||
<div class="oh-kanban-card {{card_status_class|format:instance|safe}}" {{card_attrs|format:instance|safe}}>
|
||||
<div class="oh-kanban-card__avatar">
|
||||
<div class="oh-kanban-card__profile-container">
|
||||
<img
|
||||
src="{{instance|getattribute:details.image_src}}"
|
||||
class="oh-kanban-card__profile-image"
|
||||
alt="Username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-kanban-card__details">
|
||||
<span class="oh-kanban-card__title">{{details.title|format:instance|safe}}</span>
|
||||
<span class="oh-kanban-card__subtitle">{{details.subtitle|format:instance|safe}}</span>
|
||||
</div>
|
||||
{% if actions %}
|
||||
<div class="oh-kanban-card__dots" onclick="event.stopPropagation()">
|
||||
<div class="oh-dropdown" x-data="{show: false}">
|
||||
<button
|
||||
class="oh-btn oh-btn--transparent text-muted p-3"
|
||||
@click="show = !show"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical-sharp"></ion-icon>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--dark-border oh-dropdown__menu--right"
|
||||
x-show="show"
|
||||
@click.outside="show = false"
|
||||
>
|
||||
<ul class="oh-dropdown__items">
|
||||
{% for action in actions %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a {{action.attrs|format:instance|safe}}>{{action.action}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if queryset.paginator.count %}
|
||||
<div class="oh-pagination">
|
||||
<span
|
||||
class="oh-pagination__page"
|
||||
data-toggle="modal"
|
||||
data-target="#addEmployeeModal"
|
||||
>{% trans "Page" %} {{queryset.number}} {% trans "of" %}
|
||||
{{queryset.paginator.num_pages}}</span
|
||||
>
|
||||
|
||||
<nav class="oh-pagination__nav">
|
||||
<div class="oh-pagination__input-container me-3">
|
||||
<span class="oh-pagination__label me-1">{% trans "Page" %}</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="oh-pagination__input"
|
||||
value="{{queryset.number}}"
|
||||
min="1"
|
||||
name="page"
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
/>
|
||||
<span class="oh-pagination__label"
|
||||
>{% trans "of" %} {{queryset.paginator.num_pages}}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul class="oh-pagination__items">
|
||||
{% if queryset.has_previous %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&page=1&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "First" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ queryset.previous_page_number }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Previous" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %} {% if queryset.has_next %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ queryset.next_page_number }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Next" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{request.GET.urlencode}}&page={{ queryset.paginator.num_pages }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Last" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<script>
|
||||
var tabId = $("#{{view_id}}").closest(".oh-tabs__content").attr("id");
|
||||
let badge = $(`#badge-${tabId}`);
|
||||
let count = "{{queryset.paginator.count}}";
|
||||
let label = badge.attr("data-badge-label") || "";
|
||||
let title = count + " " + label;
|
||||
badge.html(count);
|
||||
badge.attr("title", title);
|
||||
</script>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<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.">
|
||||
<h1 class="oh-404__title">{% trans "No Records found" %}</h1>
|
||||
<p class="oh-404__subtitle">
|
||||
{% trans "No records found." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
97
horilla_views/templates/generic/horilla_detailed_view.html
Normal file
97
horilla_views/templates/generic/horilla_detailed_view.html
Normal file
@@ -0,0 +1,97 @@
|
||||
{% load generic_template_filters %}
|
||||
<div class="oh-modal__dialog-header">
|
||||
<span class="oh-modal__dialog-title" id="genericModalLabel">
|
||||
{{title}}
|
||||
</span>
|
||||
<button class="oh-modal__close" aria-label="Close">
|
||||
<ion-icon name="close-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="oh-modal__dialog-body oh-modal__dialog-relative"
|
||||
style="padding-bottom: 0px"
|
||||
>
|
||||
{% if instance_ids %}
|
||||
<div class="oh-modal__dialog oh-modal__dialog--navigation m-0 p-0">
|
||||
<button
|
||||
hx-get="{{previous_url}}?{{ids_key}}={{instance_id}}&{{request.GET.urlencode}}"
|
||||
hx-target="#genericModalBody"
|
||||
class="oh-modal__diaglog-nav oh-modal__nav-prev"
|
||||
>
|
||||
<ion-icon name="chevron-back-outline"></ion-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
hx-get="{{next_url}}?{{ids_key}}={{instance_id}}&{{request.GET.urlencode}}"
|
||||
hx-target="#genericModalBody"
|
||||
class="oh-modal__diaglog-nav oh-modal__nav-next"
|
||||
>
|
||||
<ion-icon name="chevron-forward-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="keyResultsContainer">
|
||||
<div class="my-3" id="keyResultCard">
|
||||
<div class="oh-card oh-card--no-shadow oh-card__body">
|
||||
<a
|
||||
class="oh-timeoff-modal__profile-content"
|
||||
style="text-decoration: none"
|
||||
{{header.attrs|format:object|safe}}
|
||||
>
|
||||
<div class="oh-profile mb-3">
|
||||
<div class="oh-profile__avatar">
|
||||
<img
|
||||
src="{{object|getattribute:header.avatar}}"
|
||||
class="oh-profile__image me-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="oh-timeoff-modal__profile-info">
|
||||
<span class="oh-timeoff-modal__user fw-bold">
|
||||
{{object|getattribute:header.title}}
|
||||
</span>
|
||||
<span
|
||||
class="oh-timeoff-modal__user m-0"
|
||||
style="font-size: 18px; color: #4d4a4a"
|
||||
>
|
||||
{{object|getattribute:header.subtitle}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="oh-modal__dialog-header {% if header %} oh-card__footer--border-top{% endif %}"
|
||||
style="padding-top: 5px; padding-rigth: 0px; padding-left: 0px"
|
||||
>
|
||||
<div class="row">
|
||||
{% for col in body %}
|
||||
<div class="col-6 mt-3">
|
||||
<div class="oh-timeoff-modal__stat">
|
||||
<span class="oh-timeoff-modal__stat-title">{{col.0}}</span>
|
||||
<span class="oh-timeoff-modal__stat-count"
|
||||
>{{object|getattribute:col.1|safe}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-modal__dialog-footer">
|
||||
{% if actions or action_method %}
|
||||
{% if actions and not action_method %}
|
||||
<div class="oh-btn-group" style="width: 100%">
|
||||
{% for action in actions %}
|
||||
<button {{action.attrs|format:object|safe}}>
|
||||
<ion-icon name="{{action.icon}}"></ion-icon>
|
||||
{{action.action}}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %} {{object|getattribute:action_method|safe}} {% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
37
horilla_views/templates/generic/horilla_form.html
Normal file
37
horilla_views/templates/generic/horilla_form.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% for field_tuple in dynamic_create_fields %}
|
||||
<div
|
||||
class="oh-modal"
|
||||
id="dynamicModal{{field_tuple.0}}"
|
||||
role="dialog"
|
||||
aria-labelledby="dynamicModal{{field_tuple.0}}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
class="oh-modal__dialog"
|
||||
id="dynamicModal{{field_tuple.0}}Body"
|
||||
></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form id="{{view_id}}" hx-post="{{request.path}}?{{request.GET.urlencode}}" hx-swap="outerHTML">{{form.structured}}</form>
|
||||
{% for field_tuple in dynamic_create_fields %}
|
||||
<button
|
||||
hidden
|
||||
id="modalButton{{field_tuple.0}}"
|
||||
hx-get="dynamic-path-{{field_tuple.0}}-{{request.session.session_key}}?dynamic_field={{field_tuple.0}}"
|
||||
hx-target="#dynamicModal{{field_tuple.0}}Body"
|
||||
onclick="$('#dynamicModal{{field_tuple.0}}').addClass('oh-modal--show');"
|
||||
>
|
||||
{{field_tuple.0}}</button>
|
||||
<button hidden class="reload-field" hx-get="{% url "reload-field" %}?form_class_path={{form_class_path}}&dynamic_field={{field_tuple.0}}" hx-target="#dynamic_field_{{field_tuple.0}}" data-target="{{field_tuple.0}}">
|
||||
Reload Field
|
||||
</button>
|
||||
<script>
|
||||
$("#{{view_id}} [name={{field_tuple.0}}]").change(function (e) {
|
||||
if (this.value=="dynamic_create") {
|
||||
$("#modalButton{{field_tuple.0}}").click()
|
||||
$(this).val("").change();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endfor %}
|
||||
|
||||
379
horilla_views/templates/generic/horilla_list.html
Normal file
379
horilla_views/templates/generic/horilla_list.html
Normal file
@@ -0,0 +1,379 @@
|
||||
{% load static i18n generic_template_filters %}
|
||||
<div id="{{view_id|safe}}">
|
||||
<button class="reload-record" hidden hx-get="{{request.path}}?{{saved_filters.urlencode}}" hx-target="#{{view_id|safe}}" hx-swap="outerHTML">
|
||||
</button>
|
||||
{% if show_filter_tags %} {% include "generic/filter_tags.html" %} {% endif %}
|
||||
{% if queryset|length %}
|
||||
{% if bulk_select_option %}
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<div>
|
||||
<div class="oh-checkpoint-badge text-success"
|
||||
onclick="
|
||||
addToSelectedId({{select_all_ids|safe}});
|
||||
selectSelected('#{{view_id|safe}}');
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
"
|
||||
style="cursor: pointer;">
|
||||
{% trans "Select All" %}{{select_all_path}}
|
||||
</div>
|
||||
<div
|
||||
id="unselect_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-secondary d-none"
|
||||
style="cursor: pointer;"
|
||||
onclick="
|
||||
$('#selectedInstances').attr('data-ids',JSON.stringify([]));
|
||||
selectSelected('#{{view_id|safe}}');
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
$('#{{view_id}} .list-table-row').prop('checked',false);
|
||||
$('#{{view_id}} .highlight-selected').removeClass('highlight-selected');
|
||||
$('#{{view_id}} .bulk-list-table-row').prop('checked',false);
|
||||
"
|
||||
>
|
||||
{% trans "Unselect All" %}
|
||||
</div>
|
||||
<div class="oh-checkpoint-badge text-danger d-none"
|
||||
style="cursor: pointer;"
|
||||
>
|
||||
<span id="count_{{view_id}}">
|
||||
0
|
||||
</span> {% trans "Selected" %}
|
||||
</div>
|
||||
<div
|
||||
id="export_{{view_id}}"
|
||||
class="oh-checkpoint-badge text-info d-none"
|
||||
style="cursor: pointer;"
|
||||
onclick="
|
||||
selectedIds = $('#selectedInstances').attr('data-ids')
|
||||
window.location.href = '/{{export_path}}' + '?ids='+selectedIds
|
||||
"
|
||||
>
|
||||
{% trans "Export" %}
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="oh-table_sticky--wrapper">
|
||||
<div class="oh-sticky-dropdown--header" style="z-index:13;">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button class="oh-sticky-dropdown_btn" @click="open = !open">
|
||||
<ion-icon
|
||||
name="ellipsis-vertical-sharp"
|
||||
role="img"
|
||||
class="md hydrated"
|
||||
aria-label="ellipsis vertical sharp"
|
||||
></ion-icon>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-sticky-table_dropdown"
|
||||
x-show="open"
|
||||
@click.outside="
|
||||
open = false
|
||||
$($el).closest('.oh-table_sticky--wrapper').parent().find('.reload-record').click();
|
||||
"
|
||||
>
|
||||
{{toggle_form.as_list}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table">
|
||||
<div class="oh-sticky-table__table oh-table--sortable" style="
|
||||
width: 100%;
|
||||
width: -moz-available; /* WebKit-based browsers will ignore this. */
|
||||
width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
|
||||
width: fill-available;
|
||||
">
|
||||
<div class="oh-sticky-table__thead">
|
||||
<div class="oh-sticky-table__tr">
|
||||
{% if bulk_select_option %}
|
||||
<div
|
||||
class="oh-sticky-table__th"
|
||||
style="width: 10px; z-index: 12 !important"
|
||||
>
|
||||
<div class="centered-div" align="center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="oh-input oh-input__checkbox bulk-list-table-row"
|
||||
onchange="
|
||||
$(this).closest('.oh-sticky-table').find('.list-table-row').prop('checked',$(this).is(':checked')).change();
|
||||
$(document).ready(function () {
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
});
|
||||
"
|
||||
title="Select All"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% for cell in columns %}
|
||||
<div
|
||||
|
||||
class="oh-sticky-table__th"
|
||||
style="z-index: 11;width:100px;">
|
||||
<div
|
||||
{% for sort_map in sortby_mapping %}
|
||||
{% if sort_map.0 == cell.0 %}
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&{{sortby_key}}={{sort_map.1}}&filter_applied=on"
|
||||
hx-target="#{{view_id}}"
|
||||
class="
|
||||
{% if request.sort_order == "asc" and request.sort_key == sort_map.1 %}
|
||||
arrow-up
|
||||
{% elif request.sort_order == "desc" and request.sort_key == sort_map.1 %}
|
||||
arrow-down
|
||||
{% else %}
|
||||
arrow-up-down
|
||||
{% endif %}
|
||||
"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
style="width: 200px;"
|
||||
>
|
||||
{{cell.0}}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %} {% if options or option_method%}
|
||||
<div class="oh-sticky-table__th" >
|
||||
<div style="width: 200px;">
|
||||
{% trans "Options" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% if actions or action_method %}
|
||||
<div class="oh-sticky-table__th oh-sticky-table__right" style="z-index:12 !important;">
|
||||
<div style="width: 200px;">
|
||||
{% trans "Actions" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-sticky-table__tbody">
|
||||
{% for instance in queryset %}
|
||||
<div
|
||||
class="oh-sticky-table__tr oh-permission-table__tr oh-permission-table--collapsed"
|
||||
draggable="true"
|
||||
data-instance-id="{{instance.id}}"
|
||||
{{row_attrs|format:instance|safe}}
|
||||
>
|
||||
{% if bulk_select_option %}
|
||||
<div
|
||||
class="oh-sticky-table__sd {{row_status_class|format:instance|safe}}"
|
||||
onclick="event.stopPropagation()"
|
||||
style="width: 10px; z-index: 11 !important"
|
||||
>
|
||||
<div class="centered-div" align="center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="oh-input oh-input__checkbox list-table-row"
|
||||
data-view-id="{{view_id|safe}}"
|
||||
onchange="
|
||||
element = $(this)
|
||||
highlightRow(element);
|
||||
$(document).ready(function () {
|
||||
if (!element.is(':checked')) {
|
||||
removeId(element)
|
||||
}
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
});
|
||||
"
|
||||
value = "{{instance.pk}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% for cell in columns %}
|
||||
{% with attribute=cell.1 index=forloop.counter %} {% if not cell.2 %}
|
||||
<div
|
||||
class="{% if index == 1 %} oh-sticky-table__sd {% else %} oh-sticky-table__td{% endif %}"
|
||||
>
|
||||
{{instance|getattribute:attribute|safe}}
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="{% if index == 1 %} oh-sticky-table__sd {% else %} oh-sticky-table__td{% endif %}"
|
||||
>
|
||||
<div class="oh-profile oh-profile--md">
|
||||
<div class="oh-profile__avatar mr-1">
|
||||
<img
|
||||
src="{{instance|getattribute:cell.2}}"
|
||||
class="oh-profile__image"
|
||||
/>
|
||||
</div>
|
||||
<span class="oh-profile__name oh-text--dark">
|
||||
{{instance|getattribute:attribute}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endwith %} {% endfor %} {% if options or option_method %}
|
||||
<div class="oh-sticky-table__td oh-permission-table--toggle">
|
||||
{% if not option_method %}
|
||||
<div class="oh-btn-group">
|
||||
{% for option in options %}
|
||||
<a
|
||||
href="#"
|
||||
title="{{option.option|safe}}"
|
||||
{{option.attrs|format:instance|safe}}
|
||||
>
|
||||
<ion-icon name="{{option.icon}}"></ion-icon>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %} {{instance|getattribute:option_method|safe}} {% endif %}
|
||||
</div>
|
||||
{% endif %} {% if actions or action_method %}
|
||||
<div class="oh-sticky-table__td oh-sticky-table__right">
|
||||
{% if not action_method %}
|
||||
<div class="oh-btn-group">
|
||||
{% for action in actions %}
|
||||
<a
|
||||
href="#"
|
||||
title="{{action.action|safe}}"
|
||||
{{action.attrs|format:instance|safe}}
|
||||
>
|
||||
<ion-icon name="{{action.icon}}"></ion-icon>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %} {{instance|getattribute:action_method|safe}} {% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if queryset.paginator.count %}
|
||||
<div class="oh-pagination">
|
||||
<span
|
||||
class="oh-pagination__page"
|
||||
data-toggle="modal"
|
||||
data-target="#addEmployeeModal"
|
||||
>{% trans "Page" %} {{queryset.number}} {% trans "of" %}
|
||||
{{queryset.paginator.num_pages}}</span
|
||||
>
|
||||
|
||||
<nav class="oh-pagination__nav">
|
||||
<div class="oh-pagination__input-container me-3">
|
||||
<span class="oh-pagination__label me-1">{% trans "Page" %}</span>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
class="oh-pagination__input"
|
||||
value="{{queryset.number}}"
|
||||
min="1"
|
||||
name="page"
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
/>
|
||||
<span class="oh-pagination__label"
|
||||
>{% trans "of" %} {{queryset.paginator.num_pages}}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul class="oh-pagination__items" data-search-url="{{search_url}}">
|
||||
{% if queryset.has_previous %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&page=1&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "First" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&page={{ queryset.previous_page_number }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Previous" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %} {% if queryset.has_next %}
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&page={{ queryset.next_page_number }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Next" %}</a
|
||||
>
|
||||
</li>
|
||||
<li class="oh-pagination__item oh-pagination__item--wide">
|
||||
<a
|
||||
hx-get="{{search_url}}?{{saved_filters.urlencode}}&page={{ queryset.paginator.num_pages }}&filter_applied=on"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#{{view_id|safe}}"
|
||||
class="oh-pagination__link"
|
||||
>{% trans "Last" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<script>
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
var tabId = $("#{{view_id}}").closest(".oh-tabs__content").attr("id");
|
||||
let badge = $(`#badge-${tabId}`);
|
||||
let count = "{{queryset.paginator.count}}";
|
||||
let label = badge.attr("data-badge-label") || "";
|
||||
let title = count + " " + label;
|
||||
badge.html(count);
|
||||
badge.attr("title", title);
|
||||
</script>
|
||||
{% if bulk_select_option %}
|
||||
<script>
|
||||
selectSelected("#{{view_id|safe}}")
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$("ul[data-search-url] a").click(function (e) {
|
||||
e.preventDefault();
|
||||
const url = $(this).attr("hx-get")
|
||||
const $urlObj = $('<a>', { href: url });
|
||||
const searchParams = new URLSearchParams($urlObj[0].search);
|
||||
|
||||
let lastPageParam = null;
|
||||
let lastPageValue = 1;
|
||||
|
||||
searchParams.forEach((value, param) => {
|
||||
if (param === "page") {
|
||||
lastPageParam = param;
|
||||
lastPageValue = value.split(",").pop();
|
||||
}
|
||||
});
|
||||
|
||||
form = $(`form[hx-get="{{search_url}}"]`)
|
||||
pageInput = form.find("#pageInput")
|
||||
pageInput.attr("name",lastPageParam)
|
||||
pageInput.attr("value",lastPageValue)
|
||||
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<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.">
|
||||
<h1 class="oh-404__title">{% trans "No Records found" %}</h1>
|
||||
<p class="oh-404__subtitle">
|
||||
{% trans "No records found." %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
223
horilla_views/templates/generic/horilla_nav.html
Normal file
223
horilla_views/templates/generic/horilla_nav.html
Normal file
@@ -0,0 +1,223 @@
|
||||
{% load i18n %}
|
||||
<section class="oh-wrapper oh-main__topbar" x-data="{searchShow: false}">
|
||||
<div class="oh-main__titlebar oh-main__titlebar--left">
|
||||
<h1 class="oh-main__titlebar-title fw-bold mb-0">{{nav_title}}</h1>
|
||||
<a
|
||||
class="oh-main__titlebar-search-toggle"
|
||||
role="button"
|
||||
aria-label="Toggle Search"
|
||||
@click="searchShow = !searchShow"
|
||||
>
|
||||
<ion-icon
|
||||
name="search-outline"
|
||||
class="oh-main__titlebar-serach-icon"
|
||||
></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
<form autocomplete="off" id="filterForm" onsubmit="event.preventDefault()" hx-get="{{search_url}}" hx-replace-url="true" hx-target="{{search_swap_target}}" class="oh-main__titlebar oh-main__titlebar--right">
|
||||
<div class="oh-input-group oh-input__search-group" id="searchGroup">
|
||||
<ion-icon
|
||||
name="search-outline"
|
||||
class="oh-input-group__icon oh-input-group__icon--left"
|
||||
></ion-icon>
|
||||
<input
|
||||
type="text"
|
||||
class="oh-input oh-input__icon"
|
||||
name="search"
|
||||
aria-label="Search Input"
|
||||
placeholder="Search"
|
||||
autocomplete="false"
|
||||
autofocus ="true"
|
||||
onkeyup="
|
||||
$(this).closest('form').find('#applyFilter').click();
|
||||
{% if search_in %}
|
||||
$('#applyFilter')[0].click();if(this.value) {
|
||||
$('.search_text').html(this.value)
|
||||
$(this).parent().find('#dropdown').show()
|
||||
}else{
|
||||
$(this).parent().find('#dropdown').hide()
|
||||
}
|
||||
{% endif %}
|
||||
"
|
||||
{% if search_in %}
|
||||
onfocus="
|
||||
if (this.value) {
|
||||
$(this).parent().find('#dropdown').show()
|
||||
}"
|
||||
onfocusout="
|
||||
setTimeout(function() {
|
||||
$('#dropdown').hide()
|
||||
}, 300);
|
||||
"
|
||||
{% endif %}
|
||||
{{search_input_attrs|safe}}
|
||||
/>
|
||||
{% if search_in %}
|
||||
<input type="text" hidden name="search_field">
|
||||
<div class="custom-dropdown" id="dropdown">
|
||||
<ul class="search_content">
|
||||
{% for option in search_in %}
|
||||
<li>
|
||||
<a href="#" onclick="$('[name=search_field]').val('{{option.0}}'); $(this).closest('form').find('#applyFilter').click()">
|
||||
{% trans "Search" %} <b>{{option.1}}</b> {% trans "for:" %}
|
||||
<b class="search_text"></b>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="oh-main__titlebar-button-container">
|
||||
{% if view_types %}
|
||||
<ul class="oh-view-types">
|
||||
{% for type in view_types %}
|
||||
<li class="oh-view-type" onclick="$(this).closest('form').attr('hx-get','{{type.url}}');$(this).closest('form').find('#applyFilter').click();
|
||||
">
|
||||
<a class="oh-btn oh-btn--view" {{type.attrs|safe}}
|
||||
><ion-icon name="{{type.icon}}"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if filter_body_template %}
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button class="oh-btn ml-2" onclick="event.preventDefault()" @click="open = !open">
|
||||
<ion-icon name="filter" class="mr-1"></ion-icon>{% trans "Filter" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div
|
||||
class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4"
|
||||
x-show="open"
|
||||
@click.outside="open = false"
|
||||
>
|
||||
{% include filter_body_template %}
|
||||
<input type="radio" name="filter_applied" checked hidden>
|
||||
<div class="oh-dropdown__filter-footer">
|
||||
<button
|
||||
type="submit"
|
||||
id="applyFilter"
|
||||
onclick="filterFormSubmit('filterForm')"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100">
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<button
|
||||
hidden
|
||||
type="submit"
|
||||
id="applyFilter"
|
||||
class="oh-btn oh-btn--secondary oh-btn--small w-100">
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if group_by_fields %}
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button class="oh-btn ml-2" @click="open = !open" onclick="event.preventDefault()">
|
||||
<ion-icon name="library-outline" class="mr-1 md hydrated" role="img" aria-label="library outline"></ion-icon>
|
||||
{% trans "Group By" %}
|
||||
<div id="filterCount"></div>
|
||||
</button>
|
||||
<div class="oh-dropdown__menu oh-dropdown__menu--right oh-dropdown__filter p-4" x-show="open" @click.outside="open = false" style="display: none">
|
||||
<div class="oh-accordion">
|
||||
<label for="id_field">{% trans "Group By" %}</label>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="id_field">{% trans "Field" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-lg-6">
|
||||
<div class="oh-input-group">
|
||||
<select onchange="$(this).closest('form').find('#applyFilter').click()" class="oh-select mt-1 w-100" id="id_field" name="field" style="width:100%;">
|
||||
<option value="">{% trans "Select" %}</option>
|
||||
{% for field in group_by_fields %}
|
||||
<option value="{{field.0}}">{{field.1}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if actions %}
|
||||
<div class="oh-btn-group ml-2">
|
||||
<div class="oh-dropdown" x-data="{open: false}">
|
||||
<button
|
||||
onclick="event.preventDefault()"
|
||||
class="oh-btn oh-btn--dropdown"
|
||||
@click="open = !open"
|
||||
@click.outside="open = false"
|
||||
>
|
||||
{% trans "Actions" %}
|
||||
</button>
|
||||
<div class="oh-dropdown__menu oh-dropdown__menu--right" x-show="open">
|
||||
<ul class="oh-dropdown__items">
|
||||
{% for action in actions %}
|
||||
<li class="oh-dropdown__item">
|
||||
<a class="oh-dropdown__link" {{action.attrs|safe}}>{{action.action}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="hidden" id="pageInput">
|
||||
<input type="hidden" id="sortInput">
|
||||
</form>
|
||||
{% if create_attrs %}
|
||||
<a
|
||||
onclick="event.preventDefault();event.stopPropagation()"
|
||||
class="oh-btn oh-btn--secondary ml-2"
|
||||
{{create_attrs|safe}}
|
||||
>
|
||||
<ion-icon
|
||||
name="add-sharp"
|
||||
class="mr-1 md hydrated"
|
||||
role="img"
|
||||
aria-label="add sharp"
|
||||
></ion-icon
|
||||
>{% trans "Create" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<script>
|
||||
$(".oh-btn--view").click(function (e) {
|
||||
e.preventDefault();
|
||||
$(".oh-btn--view-active").removeClass("oh-btn--view-active");
|
||||
$(this).addClass("oh-btn--view-active");
|
||||
});
|
||||
if (!$(".oh-btn--view-active").length) {
|
||||
// $("a.oh-btn--view:first").trigger("click")
|
||||
}
|
||||
$(".oh-accordion-header").click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).parent().toggleClass("oh-accordion--show");
|
||||
});
|
||||
$(document).ready(function() {
|
||||
$("#filterForm").on("htmx:configRequest", function(event) {
|
||||
if (event.detail.verb == "get" && event.target.tagName == "FORM") {
|
||||
event.detail.path = $(this).attr("hx-get");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
<div id="filterTagContainerSectionNav" class="oh-titlebar-container__filters mb-2 mt-0 oh-wrapper"></div>
|
||||
<script>
|
||||
filterFormSubmit("filterForm")
|
||||
$(document).ready(function () {
|
||||
$("#filterForm select").select2("destroy")
|
||||
$("#filterForm select").parent().find("span").remove()
|
||||
$("#filterForm select").select2()
|
||||
});
|
||||
</script>
|
||||
49
horilla_views/templates/generic/horilla_section.html
Normal file
49
horilla_views/templates/generic/horilla_section.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% load static %}
|
||||
|
||||
{% for path in style_path %}
|
||||
<link rel="stylesheet" href="{{path}}"/>
|
||||
{% endfor %}
|
||||
{% for path in script_static_paths %}
|
||||
<script src="{{path}}"></script>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% include "generic/components.html" %}
|
||||
|
||||
<div
|
||||
class="oh-checkpoint-badge mb-2"
|
||||
id="selectedInstances"
|
||||
data-ids="[]"
|
||||
data-clicked=""
|
||||
style="display: none"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
hx-get="{{nav_url}}?{{request.GET.urlencode}}"
|
||||
hx-trigger="load"
|
||||
>
|
||||
<div
|
||||
class="mt-5 oh-wrapper animated-background"
|
||||
style="height:80px;"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="oh-wrapper"
|
||||
hx-get="{{view_url}}?{{request.GET.urlencode}}"
|
||||
hx-trigger="load"
|
||||
id="{{view_container_id}}"
|
||||
>
|
||||
<div
|
||||
class="mt-4 animated-background"
|
||||
style="height:600px;"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
100
horilla_views/templates/generic/horilla_tabs.html
Normal file
100
horilla_views/templates/generic/horilla_tabs.html
Normal file
@@ -0,0 +1,100 @@
|
||||
{% comment %} {% extends "index.html" %}
|
||||
{% block content %}
|
||||
{% comment %} {% include "generic/components.html" %} {% endcomment %}
|
||||
{% comment %} {% include "attendance/attendance/attendance_nav.html" %} {% endcomment %}
|
||||
{% load i18n generic_template_filters %}
|
||||
|
||||
<div class="oh-tabs">
|
||||
<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="#htv{{forloop.counter}}"
|
||||
hx-get="{{tab.url}}?{{request.GET.urlencode}}"
|
||||
hx-target="#htv{{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-htv{{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>
|
||||
</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="htv{{forloop.counter}}"
|
||||
>
|
||||
<div class="animated-background"></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) {
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
{% if active_target %}
|
||||
$("div.oh-tabs").find(`{{active_target|safe}}`).click();
|
||||
{% endif %}
|
||||
</script>
|
||||
38
horilla_views/templates/generic/index.html
Normal file
38
horilla_views/templates/generic/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "index.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
.male--custom{
|
||||
border-left: 3.4px solid rgb(32, 128, 218) !important;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
.female--custom{
|
||||
border-left: 3.4px solid pink !important;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
.male--dot{
|
||||
background-color:blue;
|
||||
}
|
||||
.female--dot{
|
||||
background-color:pink;
|
||||
}
|
||||
|
||||
.oh-tabs__tab--active{
|
||||
border-right: 1px solid hsl(213,22%,84%) !important;
|
||||
}
|
||||
</style>
|
||||
<div hx-get="{% url "horilla-navbar" %}" hx-trigger="load">
|
||||
</div>
|
||||
{% include "generic/components.html" %}
|
||||
<div
|
||||
class="oh-checkpoint-badge mb-2"
|
||||
id="selectedInstances"
|
||||
data-ids="[]"
|
||||
data-clicked=""
|
||||
style="display: none"
|
||||
>
|
||||
</div>
|
||||
{% comment %} path to the horilla tab view {% endcomment %}
|
||||
<div class="oh-wrapper" hx-get="{% url "htv" %}?{{request.GET.urlencode}}" hx-trigger="load" id="listContainer">
|
||||
<div class="animated-background"></div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
7
horilla_views/templates/generic/messages.html
Normal file
7
horilla_views/templates/generic/messages.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% if messages %}
|
||||
<div class="oh-alert-container">
|
||||
{% for message in messages %}
|
||||
<div class="oh-alert oh-alert--animated {{message.tags}}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
13
horilla_views/templates/generic/reload_select_field.html
Normal file
13
horilla_views/templates/generic/reload_select_field.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div id="{{dynamic_id}}">
|
||||
{{field}}
|
||||
</dic>
|
||||
|
||||
<script>
|
||||
$("#{{dynamic_id}} [name={{field.name}}]").change(function (e) {
|
||||
if (this.value=="dynamic_create") {
|
||||
$("#modalButton{{field.name}}").click()
|
||||
$(this).val("").change();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
0
horilla_views/templatetags/__init__.py
Normal file
0
horilla_views/templatetags/__init__.py
Normal file
61
horilla_views/templatetags/generic_template_filters.py
Normal file
61
horilla_views/templatetags/generic_template_filters.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
attendancefilters.py
|
||||
|
||||
This module is used to write custom template filters.
|
||||
|
||||
"""
|
||||
|
||||
import re, types
|
||||
from django import template
|
||||
from django.template.defaultfilters import register
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
numeric_test = re.compile("^\d+$")
|
||||
|
||||
|
||||
@register.filter(name="getattribute")
|
||||
def getattribute(value, attr: str):
|
||||
"""
|
||||
Gets an attribute of an object dynamically from a string name
|
||||
"""
|
||||
result = ""
|
||||
attrs = attr.split("__")
|
||||
for attr in attrs:
|
||||
if hasattr(value, str(attr)):
|
||||
result = getattr(value, attr)
|
||||
if isinstance(result, types.MethodType):
|
||||
result = result()
|
||||
value = result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@register.filter(name="format")
|
||||
def format(string: str, instance: object):
|
||||
"""
|
||||
format
|
||||
"""
|
||||
attr_placeholder_regex = r"{([^}]*)}"
|
||||
attr_placeholders = re.findall(attr_placeholder_regex, string)
|
||||
|
||||
if not attr_placeholders:
|
||||
return string
|
||||
flag = instance
|
||||
format_context = {}
|
||||
for attr_placeholder in attr_placeholders:
|
||||
attr_name: str = attr_placeholder
|
||||
attrs = attr_name.split("__")
|
||||
for attr in attrs:
|
||||
value = getattr(instance, attr, "")
|
||||
if isinstance(value, types.MethodType):
|
||||
value = value()
|
||||
instance = value
|
||||
format_context[attr_name] = value
|
||||
instance = flag
|
||||
formatted_string = string.format(**format_context)
|
||||
|
||||
return formatted_string
|
||||
0
horilla_views/templatetags/migrations/__init__.py
Normal file
0
horilla_views/templatetags/migrations/__init__.py
Normal file
3
horilla_views/tests.py
Normal file
3
horilla_views/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
16
horilla_views/urls.py
Normal file
16
horilla_views/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
horilla_views/urls.py
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from horilla_views import views
|
||||
from horilla_views.generic.cbv.views import ReloadMessages
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("toggle-columns", views.ToggleColumn.as_view(), name="toggle-columns"),
|
||||
path("active-tab", views.ActiveTab.as_view(), name="active-tab"),
|
||||
path("active-group", views.ActiveGroup.as_view(), name="cbv-active-group"),
|
||||
path("reload-field", views.ReloadField.as_view(), name="reload-field"),
|
||||
path("reload-messages", ReloadMessages.as_view(), name="reload-messages"),
|
||||
]
|
||||
133
horilla_views/views.py
Normal file
133
horilla_views/views.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import importlib
|
||||
from django import forms
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from horilla_views import models
|
||||
from horilla_views.cbv_methods import get_short_uuid
|
||||
from horilla_views.generic.cbv.views import dynamic_create_cache
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class ToggleColumn(View):
|
||||
"""
|
||||
ToggleColumn
|
||||
"""
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
method to toggle columns
|
||||
"""
|
||||
|
||||
query_dict = self.request.GET
|
||||
path = query_dict["path"]
|
||||
query_dict = dict(query_dict)
|
||||
del query_dict["path"]
|
||||
|
||||
hidden_fields = [key for key, value in query_dict.items() if value[0]]
|
||||
|
||||
existing_instance = models.ToggleColumn.objects.filter(
|
||||
user_id=self.request.user, path=path
|
||||
).first()
|
||||
|
||||
instance = models.ToggleColumn() if not existing_instance else existing_instance
|
||||
instance.path = path
|
||||
instance.excluded_columns = hidden_fields
|
||||
|
||||
instance.save()
|
||||
|
||||
return HttpResponse("success")
|
||||
|
||||
|
||||
class ReloadField(View):
|
||||
"""
|
||||
ReloadField
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Http method to reload dynamic create fields
|
||||
"""
|
||||
class_path = request.GET["form_class_path"]
|
||||
reload_field = request.GET["dynamic_field"]
|
||||
|
||||
module_name, class_name = class_path.rsplit(".", 1)
|
||||
module = importlib.import_module(module_name)
|
||||
parent_form = getattr(module, class_name)()
|
||||
|
||||
dynamic_cache = dynamic_create_cache.get(
|
||||
request.session.session_key + reload_field
|
||||
)
|
||||
form: forms.ModelForm = dynamic_cache["form"]
|
||||
|
||||
cache_field = dynamic_cache["dynamic_field"]
|
||||
if cache_field != reload_field:
|
||||
cache_field = reload_field
|
||||
field = parent_form.fields[cache_field]
|
||||
|
||||
queryset = form._meta.model.objects.all()
|
||||
queryset = field.queryset
|
||||
choices = [(instance.id, instance) for instance in queryset]
|
||||
choices.insert(0, ("", "Select option"))
|
||||
choices.append(("dynamic_create", "Dynamic create"))
|
||||
|
||||
parent_form.fields[cache_field] = forms.ChoiceField(
|
||||
choices=choices,
|
||||
label=field.label,
|
||||
required=field.required,
|
||||
widget=forms.Select(attrs=field.widget.attrs),
|
||||
)
|
||||
parent_form.fields[cache_field].initial = dynamic_cache["value"]
|
||||
|
||||
field = parent_form[cache_field]
|
||||
dynamic_id: str = get_short_uuid(4)
|
||||
return render(
|
||||
request,
|
||||
"generic/reload_select_field.html",
|
||||
{"field": field, "dynamic_id": dynamic_id},
|
||||
)
|
||||
|
||||
|
||||
class ActiveTab(View):
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
CBV method to handle active tab
|
||||
"""
|
||||
path = self.request.GET.get("path")
|
||||
target = self.request.GET.get("target")
|
||||
if path and target and self.request.user:
|
||||
existing_instance = models.ActiveTab.objects.filter(
|
||||
created_by=self.request.user, path=path
|
||||
).first()
|
||||
|
||||
instance = (
|
||||
models.ActiveTab() if not existing_instance else existing_instance
|
||||
)
|
||||
instance.path = path
|
||||
instance.tab_target = target
|
||||
instance.save()
|
||||
return JsonResponse({"message": "Success"})
|
||||
|
||||
|
||||
class ActiveGroup(View):
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
ActiveGroup
|
||||
"""
|
||||
path = self.request.GET.get("path")
|
||||
target = self.request.GET.get("target")
|
||||
group_field = self.request.GET.get("field")
|
||||
if path and target and group_field and self.request.user:
|
||||
existing_instance = models.ActiveGroup.objects.filter(
|
||||
created_by=self.request.user,
|
||||
path=path,
|
||||
group_by_field=group_field,
|
||||
).first()
|
||||
|
||||
instance = (
|
||||
models.ActiveGroup() if not existing_instance else existing_instance
|
||||
)
|
||||
instance.path = path
|
||||
instance.group_by_field = group_field
|
||||
instance.group_target = target
|
||||
instance.save()
|
||||
return JsonResponse({"message": "Success"})
|
||||
@@ -437,6 +437,10 @@ class Candidate(HorillaModel):
|
||||
"""
|
||||
return self.email
|
||||
|
||||
def get_mail(self):
|
||||
""" """
|
||||
return self.get_email()
|
||||
|
||||
def tracking(self):
|
||||
"""
|
||||
This method is used to return the tracked history of the instance
|
||||
@@ -523,6 +527,9 @@ class Candidate(HorillaModel):
|
||||
ordering = ["sequence"]
|
||||
|
||||
|
||||
from horilla.signals import pre_bulk_update
|
||||
|
||||
|
||||
class RejectReason(HorillaModel):
|
||||
"""
|
||||
RejectReason
|
||||
|
||||
39
static/build/js/clearFilter.js
Normal file
39
static/build/js/clearFilter.js
Normal file
@@ -0,0 +1,39 @@
|
||||
var formButton = "#applyFilter";
|
||||
function clearFilterFromTag(element) {
|
||||
let field_id = element.attr("data-x-field");
|
||||
$(`[name=${field_id}]`).val("");
|
||||
$(`[name=${field_id}]`).change();
|
||||
// Update all elements with the same ID to have null values
|
||||
let elementId = $(`[name=${field_id}]:last`).attr("id");
|
||||
let spanElement = $(
|
||||
`.oh-dropdown__filter-body:first #select2-id_${field_id}-container, #select2-${elementId}-container`
|
||||
);
|
||||
if (spanElement.length) {
|
||||
spanElement.attr("title", "---------");
|
||||
spanElement.text("---------");
|
||||
}
|
||||
$(formButton).click();
|
||||
console.log($(formButton));
|
||||
}
|
||||
function clearAllFilter(element) {
|
||||
$('[role="tooltip"]').remove();
|
||||
let field_ids = $("[data-x-field]");
|
||||
for (var i = 0; i < field_ids.length; i++) {
|
||||
let item_id = field_ids[i].getAttribute("data-x-field");
|
||||
|
||||
$(`[name=${item_id}]`).val("");
|
||||
$(`[name=${item_id}]`).change();
|
||||
let elementId = $(`[name=${item_id}]:last`).attr("id");
|
||||
let spanElement = $(
|
||||
`.oh-dropdown__filter-body:first #select2-id_${item_id}-container, #select2-${elementId}-container`
|
||||
);
|
||||
if (spanElement.length) {
|
||||
spanElement.attr("title", "---------");
|
||||
spanElement.text("---------");
|
||||
}
|
||||
$(formButton).click();
|
||||
localStorage.removeItem("savedFilters");
|
||||
var url = window.location.href.split("?")[0];
|
||||
window.history.replaceState({}, document.title, url);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
<script src="{% static '/base/toggleColumn.js' %}"></script>
|
||||
<script src="{% static '/build/js/orgChart.js' %}"></script>
|
||||
<script src="{% static 'build/js/driver.js' %}"></script>
|
||||
<script src="{% static "build/js/clearFilter.js" %}"></script>
|
||||
|
||||
|
||||
<!-- Popper.JS -->
|
||||
@@ -54,6 +55,9 @@
|
||||
{% comment %} </div> {% endcomment %}
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<span class="logged-in" data-user-id="{{request.user.id}}"></span>
|
||||
<div id="reloadMessages">
|
||||
{% include "generic/messages.html" %}
|
||||
</div>
|
||||
{% if messages %}
|
||||
<div class="oh-alert-container">
|
||||
{% for message in messages %}
|
||||
@@ -63,7 +67,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button id="reloadMessagesButton" hx-get="{% url 'reload-messages' %}" hidden hx-target="#reloadMessages"></button>
|
||||
|
||||
<div class="oh-wrapper-main" :class="!sidebarOpen ? 'oh-wrapper-main--closed' : ''" x-data="{sidebarOpen: true}"
|
||||
@load.window="
|
||||
@@ -434,7 +438,7 @@
|
||||
$(document).on("htmx:beforeRequest", function (event, data) {
|
||||
var response = event.detail.xhr.response;
|
||||
var target = $(event.detail.elt.getAttribute("hx-target"));
|
||||
var avoid_target = ["BiometricDeviceTestFormTarget"];
|
||||
var avoid_target = ["BiometricDeviceTestFormTarget","reloadMessages"];
|
||||
if (!target.closest("form").length && avoid_target.indexOf(target.attr("id")) === -1) {
|
||||
target.html(`<div class="animated-background"></div>`);
|
||||
}
|
||||
@@ -531,6 +535,76 @@
|
||||
setTimeout(()=>{$("[name='search']").focus()},100)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function addToSelectedId(newIds){
|
||||
ids = JSON.parse(
|
||||
$("#selectedInstances").attr("data-ids") || "[]"
|
||||
);
|
||||
|
||||
ids = [...ids,...newIds.map(String)]
|
||||
ids = Array.from(new Set(ids));
|
||||
$("#selectedInstances").attr("data-ids",JSON.stringify(ids))
|
||||
}
|
||||
function selectSelected(viewId){
|
||||
ids = JSON.parse(
|
||||
$("#selectedInstances").attr("data-ids") || "[]"
|
||||
);
|
||||
$.each(ids, function (indexInArray, valueOfElement) {
|
||||
$(`${viewId} .oh-sticky-table__tbody .list-table-row[value=${valueOfElement}]`).prop("checked",true).change()
|
||||
});
|
||||
$(`${viewId} .oh-sticky-table__tbody .list-table-row`).change(function (
|
||||
e
|
||||
) {
|
||||
id = $(this).val()
|
||||
ids = JSON.parse(
|
||||
$("#selectedInstances").attr("data-ids") || "[]"
|
||||
);
|
||||
ids = Array.from(new Set(ids));
|
||||
let index = ids.indexOf(id);
|
||||
if (!ids.includes(id)) {
|
||||
ids.push(id);
|
||||
} else {
|
||||
if (!$(this).is(":checked")) {
|
||||
ids.splice(index, 1);
|
||||
}
|
||||
}
|
||||
$("#selectedInstances").attr("data-ids", JSON.stringify(ids));
|
||||
}
|
||||
);
|
||||
reloadSelectedCount($('#count_{{view_id|safe}}'));
|
||||
|
||||
}
|
||||
function reloadSelectedCount(targetElement) {
|
||||
count = JSON.parse($("#selectedInstances").attr("data-ids") || "[]").length
|
||||
id =targetElement.attr("id")
|
||||
if (id) {
|
||||
id =id.split("count_")[1]
|
||||
}
|
||||
if (count) {
|
||||
targetElement.html(count)
|
||||
targetElement.parent().removeClass("d-none");
|
||||
$(`#unselect_${id}, #export_${id}`).removeClass("d-none");
|
||||
|
||||
|
||||
}else{
|
||||
targetElement.parent().addClass("d-none")
|
||||
$(`#unselect_${id}, #export_${id}`).addClass("d-none")
|
||||
|
||||
}
|
||||
}
|
||||
function removeId(element){
|
||||
id = element.val();
|
||||
viewId = element.attr("data-view-id")
|
||||
ids = JSON.parse($("#selectedInstances").attr("data-ids") || "[]")
|
||||
let elementToRemove = 5;
|
||||
if (ids[ids.length - 1] === id) {
|
||||
ids.pop();
|
||||
}
|
||||
ids = JSON.stringify(ids)
|
||||
$("#selectedInstances").attr("data-ids", ids);
|
||||
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -248,6 +248,16 @@ function initializeSummernote(candId,searchWords,) {
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.recruitment.view_mailautomation %}
|
||||
<li class="oh-sidebar__submenu-item">
|
||||
<a
|
||||
class="oh-sidebar__submenu-link"
|
||||
href="{% url 'mail-automations' %}"
|
||||
class="oh-sidebar__submenu-link"
|
||||
>{% trans "Mail Automations" %}</a
|
||||
>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.leave.add_holiday %}
|
||||
<li class="oh-sidebar__submenu-item">
|
||||
<a
|
||||
@@ -457,4 +467,23 @@ function initializeSummernote(candId,searchWords,) {
|
||||
}
|
||||
$(this).closest("form").submit();
|
||||
}
|
||||
|
||||
function filterFormSubmit(formId) {
|
||||
var formData = $("#" + formId).serialize();
|
||||
var count = 0;
|
||||
formData.split("&").forEach(function (field) {
|
||||
var parts = field.split("=");
|
||||
var value = parts[1];
|
||||
if (parts[0] !== "view" && parts[0] !== "filter_applied"){
|
||||
if (value && value !== "unknown") {
|
||||
console.log(parts[0],parts[1])
|
||||
count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
$("#filterCount").empty();
|
||||
if (count > 0) {
|
||||
$("#filterCount").text(`(${count})`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user