[IMP] AUTOMATIONS: Add horilla automations

This commit is contained in:
Horilla
2024-06-12 16:44:05 +05:30
parent 180d1d4dfa
commit 2ee8495f5a
68 changed files with 4807 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,9 @@
"""
horilla/signals.py
"""
from django.dispatch import Signal, receiver
pre_bulk_update = Signal()
post_bulk_update = Signal()

View File

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

View File

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from horilla_automations.models import MailAutomation
# Register your models here.
admin.site.register(
[
MailAutomation,
]
)

View 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

View 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__"

View 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)

View File

View 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

View 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

View File

@@ -0,0 +1 @@

View 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()

View 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()

View 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");
}
});
}

View File

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

View File

@@ -0,0 +1,3 @@
{% extends "index.html" %}
{% block content %}
{% endblock content %}

View File

@@ -0,0 +1 @@
{{instance.condition_html|safe|escape }}

View File

@@ -0,0 +1,5 @@
<ol>
{% for to in mappings %}
<li>{{to}}</li>
{% endfor %}
</ol>

View File

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

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View 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",
),
]

View File

View 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}')
"
""",
},
]

View 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"))

View File

10
horilla_views/admin.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class HorillaViewsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'horilla_views'

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

View File

View File

View 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

View File

95
horilla_views/models.py Normal file
View 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()

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

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

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

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

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

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

View File

@@ -0,0 +1,3 @@
<button>
Hello
</button>

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

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

View 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 %}

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

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

View 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 %}

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

View 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 %}

View 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 %}

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

View File

View 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

3
horilla_views/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
horilla_views/urls.py Normal file
View 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
View 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"})

View File

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

View 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);
}
}

View File

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

View File

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