[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

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