[UPDT] GENERAL: Eval method change (#397)

This commit is contained in:
Horilla
2024-11-26 14:24:55 +05:30
parent b0b355f10d
commit 489eded955
26 changed files with 749 additions and 61 deletions

View File

@@ -30,6 +30,7 @@ from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.utils.translation import gettext_lazy as _trans
from base.methods import eval_validate
from horilla import settings
from horilla.horilla_middlewares import _thread_locals
from horilla_views.templatetags.generic_template_filters import getattribute
@@ -487,5 +488,24 @@ def value_to_field(field: object, value: list) -> Any:
):
value = value[0]
return value
value = eval(str(value[0]))
value = eval_validate(str(value[0]))
return value
def merge_dicts(dict1, dict2):
"""
Method to merge two dicts
"""
merged_dict = dict1.copy()
for key, value in dict2.items():
if key in merged_dict:
for model_class, instances in value.items():
if model_class in merged_dict[key]:
merged_dict[key][model_class].extend(instances)
else:
merged_dict[key][model_class] = instances
else:
merged_dict[key] = value
return merged_dict

View File

@@ -5,9 +5,9 @@ horilla_views/forms.py
import os
from django import forms
from django.contrib import messages
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db import transaction
from django.template.loader import render_to_string
from django.utils.safestring import SafeText
from django.utils.translation import gettext_lazy as _trans
@@ -20,7 +20,6 @@ from horilla_views.cbv_methods import (
MODEL_FORM_FIELD_MAP,
get_field_class_map,
structured,
value_to_field,
)
from horilla_views.templatetags.generic_template_filters import getattribute
@@ -105,7 +104,7 @@ class DynamicBulkUpdateForm(forms.Form):
root_model: models.models.Model = None,
bulk_update_fields: list = [],
ids: list = [],
**kwargs
**kwargs,
):
self.ids = ids
self.root_model = root_model
@@ -146,7 +145,6 @@ class DynamicBulkUpdateForm(forms.Form):
self.fields[key].widget.option_template_name = (
"horilla_widgets/select_option.html",
)
print(self.fields[key].empty_values)
continue
self.fields[key] = field(
widget=widget,
@@ -168,6 +166,21 @@ class DynamicBulkUpdateForm(forms.Form):
"horilla_widgets/select_option.html",
)
def is_valid(self):
valid = True
try:
with transaction.atomic():
# Perform bulk update
self.save()
# Simulate error check
raise Exception("no_errors")
except Exception as e:
# Handle errors or validation issues
if not "no_errors" in str(e):
valid = False
self.add_error(None, f"Form not valid: {str(e)}")
return valid
def save(self, *args, **kwargs):
"""
Bulk save method
@@ -243,5 +256,3 @@ class DynamicBulkUpdateForm(forms.Form):
for field, file in files.items():
file_path = os.path.join(field.upload_to, file.name)
default_storage.save(file_path, ContentFile(file.read()))
messages.success(self.request, _trans("Selected Records updated"))

View File

@@ -18,7 +18,7 @@ from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _trans
from django.views.generic import DetailView, FormView, ListView, TemplateView
from base.methods import closest_numbers, get_key_instances
from base.methods import closest_numbers, eval_validate, get_key_instances
from horilla.filters import FilterSet
from horilla.group_by import group_by_queryset
from horilla.horilla_middlewares import _thread_locals
@@ -142,7 +142,7 @@ class HorillaListView(ListView):
form = self.get_bulk_form()
form.verbose_name = (
form.verbose_name
+ f" ({len((eval(request.GET.get('instance_ids','[]'))))} {_trans('Records')})"
+ f" ({len((eval_validate(request.GET.get('instance_ids','[]'))))} {_trans('Records')})"
)
return render(
request,
@@ -158,7 +158,7 @@ class HorillaListView(ListView):
return HttpResponse("You dont have permission")
instance_ids = request.GET.get("instance_ids", "[]")
instance_ids = eval(instance_ids)
instance_ids = eval_validate(instance_ids)
form = DynamicBulkUpdateForm(
request.POST,
request.FILES,
@@ -168,6 +168,7 @@ class HorillaListView(ListView):
)
if instance_ids and form.is_valid():
form.save()
messages.success(request, _trans("Selected Records updated"))
script_id = get_short_uuid(length=3, prefix="bulk")
return HttpResponse(
@@ -219,7 +220,7 @@ class HorillaListView(ListView):
is_default=True,
).first()
if not bool(query_dict) and default_filter:
data = eval(default_filter.filter)
data = eval_validate(default_filter.filter)
query_dict = QueryDict("", mutable=True)
for key, value in data.items():
query_dict[key] = value
@@ -395,8 +396,8 @@ class HorillaListView(ListView):
from import_export import fields, resources
request = getattr(_thread_locals, "request", None)
ids = eval(request.GET["ids"])
_columns = eval(request.GET["columns"])
ids = eval_validate(request.GET["ids"])
_columns = eval_validate(request.GET["columns"])
queryset = self.model.objects.filter(id__in=ids)
_model = self.model
@@ -515,7 +516,7 @@ class HorillaDetailedView(DetailView):
def get_context_data(self, **kwargs: Any):
context = super().get_context_data(**kwargs)
instance_ids = eval(str(self.request.GET.get(self.ids_key)))
instance_ids = eval_validate(str(self.request.GET.get(self.ids_key)))
pk = context["object"].pk
if instance_ids:
@@ -663,7 +664,7 @@ class HorillaCardView(ListView):
is_default=True,
).first()
if not bool(query_dict) and default_filter:
data = eval(default_filter.filter)
data = eval_validate(default_filter.filter)
query_dict = QueryDict("", mutable=True)
for key, value in data.items():
query_dict[key] = value
@@ -866,7 +867,7 @@ class HorillaFormView(FormView):
pk = self.form.instance.pk
# next/previous option in the forms
if pk and self.request.GET.get(self.ids_key):
instance_ids = eval(str(self.request.GET.get(self.ids_key)))
instance_ids = eval_validate(str(self.request.GET.get(self.ids_key)))
url = resolve(self.request.path)
key = list(url.kwargs.keys())[0]
url_name = url.url_name
@@ -1140,7 +1141,7 @@ class HorillaProfileView(DetailView):
instance_ids_str = self.request.GET.get("instance_ids")
if not instance_ids_str:
instance_ids_str = "[]"
instance_ids = eval(instance_ids_str)
instance_ids = eval_validate(instance_ids_str)
if instance_ids:
CACHE.set(
f"{self.request.session.session_key}hpv-instance-ids", instance_ids

View File

@@ -1,3 +1,12 @@
<div class="oh-modal" id="deleteConfirmation" role="dialog" aria-labelledby="deleteConfirmation" aria-hidden="true">
<div class="oh-modal__dialog oh-modal__dialog--custom" id="deleteConfirmationBody">
</div>
</div>
<button hx-get="{% url "generic-delete" %}?model=employee.Employee&pk=13" hx-target="#deleteConfirmationBody" data-toggle="oh-modal-toggle" data-target="#deleteConfirmation">
Delete
</button>
<div
class="oh-modal"
id="genericModal"

View File

@@ -0,0 +1,237 @@
{% load generic_template_filters i18n %}
<script>
$("#reloadMessagesButton").click()
</script>
<button hidden class="reload-record" hx-get="{% url "generic-delete" %}?{{request.GET.urlencode}}" hx-target="#{{confirmation_target}}">{{request.GET.urlencode}}</button>
<div class="oh-modal__dialog-header">
<span class="oh-modal__dialog-title" id="deleteConfirmationLabel">{% trans "Delete Confirmation" %}</span>
<button class="oh-modal__close--custom" onclick="$(this).closest('.oh-modal--show').removeClass('oh-modal--show')" aria-label="Close"><ion-icon name="close-outline" role="img" class="md hydrated" aria-label="close outline"></ion-icon></button>
</div>
<div class="oh-modal__dialog-body oh-modal__dialog-relative" style="padding-bottom: 0px">
<div class="">
<div class="oh-card">
<div class="row">
<div class="col-12 col-sm-12 col-md-12 col-lg-12">
<ul class="oh-general__tabs oh-general__tabs--border oh-general__tabs--profile oh-general__tabs--no-grow oh-profile-section__tab mt-2">
<li class="oh-general__tab">
<a href="#" class="oh-general__tab-link" data-action="general-tab" data-target="#summary">{% trans "Summary" %}</a>
</li>
{% for key in model_map.keys %}
<li class="oh-general__tab">
<a href="#" class="oh-general__tab-link" data-action="general-tab" data-target="#{{key}}">{{key}}</a>
</li>
{% endfor %}
</ul>
</div>
<div class="oh-general__tab-target oh-profile__info-tab" id="summary">
<style>
.check-list {
margin: 0;
padding-left: 1.2rem;
}
.check-list li {
position: relative;
list-style-type: none;
padding-left: 2.5rem;
margin-bottom: 0.5rem;
cursor: pointer;
}
.check-list li:hover {
background-color: #e5ffff;
}
.oh-inner-sidebar__link{
cursor: pointer;
}
/* Checkmark styling */
.check-list li:not(li.x-marked):before {
content: '';
display: block;
position: absolute;
left: 0;
top: -2px;
width: 5px;
height: 11px;
border-width: 0 2px 2px 0;
border-style: solid;
border-color: #00a8a8;
transform-origin: bottom left;
transform: rotate(45deg);
}
/* X-mark styling */
.check-list .x-marked:before {
content: '';
display: block;
position: absolute;
left: 0;
top: -1px;
width: 10px;
height: 10px;
background: linear-gradient(
45deg,
transparent 46%,
red 46%,
red 54%,
transparent 54%
),
linear-gradient(
-45deg,
transparent 46%,
red 46%,
red 54%,
transparent 54%
);
}
</style>
<div>
<h5 class="mt-3 mb-2">
{% trans "Deleting the record" %} '{{delete_object}}' {% trans "would require managing the following related objects:" %}
</h5>
<h6>
{% trans "Protected Records" %} ({{protected|length}})
</h6>
<ul class="check-list">
{% for summary in protected_objects_count.items %}
<li
onclick="
{% if "-" not in summary.0 %}
$(`#{{summary.0|get_id|slice:":-1"}}`).click()
{% else %}
$(`#{{summary.0|get_id}}`).click()
{% endif %}
"
>{{summary.0|capfirst}} : {{summary.1|capfirst}}</li>
{% endfor %}
</ul>
<h6>
{% trans "Other Related Records" %} ({{model_count_sum}})
</h6>
<ul class="check-list">
{% for summary in related_objects_count.items %}
<li
onclick="
{% if "-" not in summary.0 %}
$(`#{{summary.0|get_id|slice:":-1"}}`).click()
{% else %}
$(`#{{summary.0|get_id}}`).click()
{% endif %}
"
>{{summary.0|capfirst}} : {{summary.1|capfirst}}</li>
{% endfor %}
</ul>
<form hx-post="{% url "generic-delete" %}?{{request.GET.urlencode}}" hx-target="#deleteConfirmationBody">
<div class="d-flex justify-content-end">
<div>
<div class="mt-2">
<input type="checkbox" required id="action">
<label for="action">
{% trans "I Have took manual action for the protected records" %}
</label>
</div>
<div class="mt-2">
<input type="checkbox" required id="revert">
<label for="revert">
{% trans "I acknowledge, I wont be able to revert this " %}
</label>
</div>
<div class="mt-2">
<input type="checkbox" required id="confirm">
<label for="confirm">
{% trans "Confirming to delete the related and protected records" %}
</label>
</div>
</div>
<div style="margin-top: 30px;">
<button type="submit" class="oh-btn oh-btn--secondary m-2">{% trans "Delete" %}</button>
</div>
</div>
</form>
</div>
</div>
{% for key in model_map.keys %}
<div class="oh-general__tab-target oh-profile__info-tab" id="{{key}}">
<div class="row">
<div class="col-12 col-sm-12 col-md-12 col-lg-3" style="width: 35% !important;">
<div class="oh-inner-sidebar oh-resp-hidden--lg" id="mobileMenu">
<ul class="oh-inner-sidebar__items">
{% with models_dict=model_map|get_item:key %}
{% for item in models_dict.items %}
<li class="oh-inner-sidebar__item" id="{{item.0.verbose_name|lower}}item" onclick="
localStorage.setItem('DeletenavItem','#'+$(this).attr('id'))
">
<a
id="{{item.0.verbose_name|lower}}"
onclick="$(`[data-target='#{{key}}']`).click();$(this).parent().find('button').click()"
class="oh-inner-sidebar__link">{{item.0.verbose_name}}</a>
<button
hidden
hx-get="/{{dynamic_list_path|get_item:item.0.verbose_name}}"
hx-target="#dynamicRelatedLists{{key}}"
></button>
<div id="storedIds{{key}}{{item.0.verbose_name}}" data-ids="[]"></div>
</li>
{% endfor %}
{% endwith %}
</ul>
</div>
</div>
<div class="col-12 col-sm-12 col-md-12 col-lg-9" style="width: 65% !important;">
<div class="oh-inner-sidebar-content">
<div class="oh-inner-sidebar-content__header mt-3">
<h2 class="oh-inner-sidebar-content__title">{% trans "Action Required" %}⚠️</h2>
</div>
<div class="oh-inner-sidebar-content__body">
<div class="row">
<div class="col-sm-12 col-md-12 col lg-12" id="dynamicRelatedLists{{key}}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="oh-modal__dialog-footer"></div>
<script>
$(".oh-general__tab-link").click(function (e) {
e.preventDefault();
$("#deleteConfirmationBody .oh-general__tab-link--active").removeClass("oh-general__tab-link--active");
$(this).addClass("oh-general__tab-link--active");
var target = $(this).attr("data-target");
$('#deleteConfirmationBody .oh-profile__info-tab').addClass("d-none");
$(`#deleteConfirmationBody ${target}`).removeClass("d-none");
localStorage.setItem("deleteConfirmation",target)
});
$(".oh-inner-sidebar__link").click(function (e) {
e.preventDefault();
$(this).closest("ul").find(".oh-inner-sidebar__link--active").removeClass("oh-inner-sidebar__link--active");
$(this).addClass("oh-inner-sidebar__link--active");
});
target = localStorage.getItem("deleteConfirmation",null)
navTarget = localStorage.getItem("DeletenavItem",null)
if(target && $(`#deleteConfirmationBody .oh-general__tab-link[data-target='${target}']`).length){
$(`#deleteConfirmationBody .oh-general__tab-link[data-target='${target}']`).click();
if(navTarget){
setTimeout(() => {
$(`${navTarget}`).addClass('oh-inner-sidebar__link--active');
$(`${navTarget} a`).addClass('oh-inner-sidebar__link--active');
$(`${navTarget} button`).click();
}, 100);
}
}
if(!$("#deleteConfirmationBody .oh-general__tab-link--active:first").length){
$(`#deleteConfirmationBody .oh-general__tab-link:first`).click();
}
</script>

View File

@@ -139,3 +139,11 @@ def get_item(dictionary: dict, key: str):
if dictionary:
return dictionary.get(key, "")
return ""
@register.filter("get_id")
def get_id(string: str):
"""
Generate target/id for the generic delete summary
"""
return string.split("-")[0].lower().replace(" ", "")

View File

@@ -39,4 +39,9 @@ urlpatterns = [
views.LastAppliedFilter.as_view(),
name="last-applied-filter",
),
path(
"generic-delete",
views.HorillaDeleteConfirmationView.as_view(),
name="generic-delete",
),
]

View File

@@ -1,18 +1,23 @@
import importlib
from collections import defaultdict
from django import forms
from django.apps import apps
from django.contrib import messages
from django.contrib.admin.utils import NestedObjects
from django.core.cache import cache as CACHE
from django.db import router
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_protect
from base.methods import eval_validate
from horilla_views import models
from horilla_views.cbv_methods import get_short_uuid, login_required
from horilla_views.cbv_methods import get_short_uuid, login_required, merge_dicts
from horilla_views.forms import SavedFilterForm
from horilla_views.generic.cbv.views import HorillaFormView
from horilla_views.generic.cbv.views import HorillaFormView, HorillaListView
# Create your views here.
@@ -93,7 +98,7 @@ class ReloadField(View):
)
dynamic_initial = request.GET.get("dynamic_initial", [])
parent_form.fields[cache_field].widget.attrs = field.widget.attrs
parent_form.fields[cache_field].initial = eval(
parent_form.fields[cache_field].initial = eval_validate(
f"""[{dynamic_cache["value"]},{dynamic_initial}]"""
)
@@ -258,3 +263,302 @@ class LastAppliedFilter(View):
timeout=600,
)
return HttpResponse("success")
class DynamiListView(HorillaListView):
"""
DynamicListView for Generic Delete
"""
instances = []
def get_queryset(self):
search = self.request.GET.get("search", "")
def _search_filter(instance):
return search in str(instance).lower()
return filter(_search_filter, self.instances)
class HorillaDeleteConfirmationView(View):
"""
Generic Delete Confirmation View
"""
confirmation_target = "deleteConfirmationBody"
def get(self, *args, **kwargs):
"""
GET method
"""
from horilla.urls import path, urlpatterns
pk = self.request.GET["pk"]
app, MODEL_NAME = self.request.GET["model"].split(".")
if not self.request.user.has_perm(app + ".delete_" + MODEL_NAME.lower()):
return render(self.request, "no_perm.html")
model = apps.get_model(app, MODEL_NAME)
delete_object = model.objects.get(pk=pk)
objs = [delete_object]
using = router.db_for_write(delete_object._meta.model)
collector = NestedObjects(using=using, origin=objs)
collector.collect(objs)
MODEL_MAP = {}
PROTECTED_MODEL_MAP = {}
DYNAMIC_PATH_MAP = {}
MODEL_RELATED_FIELD_MAP = {}
MODEL_RELATED_PROTECTED_FIELD_MAP = {}
def format_callback(instance, protected=False):
if not MODEL_RELATED_FIELD_MAP.get(instance._meta.model):
MODEL_RELATED_FIELD_MAP[instance._meta.model] = []
MODEL_RELATED_PROTECTED_FIELD_MAP[instance._meta.model] = []
def find_related_field(obj, related_instance):
for field in obj._meta.get_fields():
# Check if the field is a foreign key (or related model)
if isinstance(
field, (models.models.ForeignKey, models.models.OneToOneField)
):
# Get the field value
field_value = getattr(obj, field.name)
# If the field value matches the related instance, return the field name
if field_value == related_instance:
if "PROTECT" in field.remote_field.on_delete.__name__:
MODEL_RELATED_PROTECTED_FIELD_MAP[
instance._meta.model
].append((field.name, field.verbose_name))
MODEL_RELATED_FIELD_MAP[instance._meta.model].append(
field.name
)
find_related_field(instance, delete_object)
app_label = instance._meta.app_label
app_label = apps.get_app_config(app_label).verbose_name
model = instance._meta.model
model.verbose_name = model.__name__.split("_")[0]
model_map = PROTECTED_MODEL_MAP if protected else MODEL_MAP
if app_label not in model_map:
model_map[app_label] = {}
if model not in model_map[app_label]:
model_map[app_label][model] = []
DYNAMIC_PATH_MAP[model.verbose_name] = (
f"{get_short_uuid(prefix='generic-delete',length=10)}"
)
class DynamiListView(HorillaListView):
"""
DynamicListView for Generic Delete
"""
instances = []
columns = [
(
"Record",
"dynamic_display_name_generic_delete",
),
]
records_per_page = 5
selected_instances_key_id = "storedIds" + app_label
def dynamic_display_name_generic_delete(self):
is_protected = False
classname = self.__class__.__name__
app_label = self._meta.app_label
app_verbose_name = apps.get_app_config(app_label).verbose_name
protected = PROTECTED_MODEL_MAP.get(app_verbose_name, {}).get(
self._meta.model, []
)
ids = [instance.pk for instance in protected]
if self.pk in ids:
is_protected = True
if "_" in classname:
field_name = classname.split("_", 1)[1]
classname = classname.split("_")[0]
object_field_name = classname.lower()
model = apps.get_model(app_label, classname)
field = model._meta.get_field(field_name)
return f"""
{getattr(self, object_field_name)}
<i style="color:#989898;">(In {field.verbose_name})</i>
"""
indication = f"""
{self}
"""
if is_protected:
verbose_names = [
str(i[1])
for i in list(
set(
MODEL_RELATED_PROTECTED_FIELD_MAP.get(
self._meta.model, ""
)
)
)
]
indication = (
indication
+ f"""
<i style="color:red;">(Record in {",".join(verbose_names)})</i>
"""
)
return indication
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._saved_filters = self.request.GET
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["search_url"] = "/" + self.search_url
return context
def get_queryset(self):
search = self.request.GET.get("search", "")
def _search_filter(instance):
return search in str(instance).lower()
self.instances = list(
set(
(
self.instances
+ MODEL_MAP.get(
apps.get_app_config(
self.model._meta.app_label
).verbose_name,
{},
).get(self.model, [])
+ PROTECTED_MODEL_MAP.get(
apps.get_app_config(
self.model._meta.app_label
).verbose_name,
{},
).get(self.model, [])
)
)
)
queryset = self.model.objects.filter(
pk__in=[
instance.pk
for instance in filter(_search_filter, self.instances)
]
)
return queryset
model.dynamic_display_name_generic_delete = (
DynamiListView.dynamic_display_name_generic_delete
)
DynamiListView.model = model
if "_" in model.__name__:
DynamiListView.bulk_update_fields = [MODEL_NAME.lower()]
else:
DynamiListView.bulk_update_fields = MODEL_RELATED_FIELD_MAP.get(
model, []
)
DynamiListView.instances = model_map[app_label][model]
DynamiListView.search_url = DYNAMIC_PATH_MAP[model.verbose_name]
DynamiListView.selected_instances_key_id = (
DynamiListView.selected_instances_key_id + model.verbose_name
)
urlpatterns.append(
path(
DynamiListView.search_url,
DynamiListView.as_view(),
name=DynamiListView.search_url,
)
)
model_map[app_label][model].append(instance)
return instance
_to_delete = collector.nested(format_callback)
protected = [
format_callback(obj, protected=True) for obj in collector.protected
]
model_count = {
model._meta.verbose_name_plural: len(objs)
for model, objs in collector.model_objs.items()
}
protected_model_count = defaultdict(int)
for obj in collector.protected:
model = type(obj)
protected_model_count[model._meta.verbose_name_plural] += 1
protected_model_count = dict(protected_model_count)
return render(
self.request,
"generic/delete_confirmation.html",
{
"model_map": merge_dicts(MODEL_MAP, PROTECTED_MODEL_MAP),
"dynamic_list_path": DYNAMIC_PATH_MAP,
"delete_object": delete_object,
"protected": protected,
"model_count_sum": sum(model_count.values()),
"related_objects_count": model_count,
"protected_objects_count": protected_model_count,
}
| self.get_context_data(),
)
def post(self, *args, **kwargs):
"""
Post method to handle the delete
"""
pk = self.request.GET["pk"]
app, MODEL_NAME = self.request.GET["model"].split(".")
if not self.request.user.has_perm(app + ".delete_" + MODEL_NAME.lower()):
return render(self.request, "no_perm.html")
model = apps.get_model(app, MODEL_NAME)
delete_object = model.objects.get(pk=pk)
objs = [delete_object]
using = router.db_for_write(delete_object._meta.model)
collector = NestedObjects(using=using, origin=objs)
collector.collect(objs)
def delete_callback(instance, protected=False):
try:
instance.delete()
messages.success(self.request, f"Deleted {instance}")
except:
messages.error(self.request, f"Cannot delete : {instance}")
# deleting protected objects
for obj in collector.protected:
delete_callback(obj, protected=True)
# deleting related objects
collector.nested(delete_callback)
return HttpResponse(
"""
<script>
$("#reloadMessagesButton").click();
$(".oh-modal--show").removeClass("oh-modal--show");
</script>
"""
)
def get_context_data(self, **kwargs) -> dict:
context = {}
context["confirmation_target"] = self.confirmation_target
return context